Compare commits

..

2188 Commits

Author SHA1 Message Date
CyrilWaechter 9cd0f837a0 Fix space regen Z placement for centered representations
get_x_y_z_h_mat_from_obj computes z from bound_box[0], which differs from obj.location.z when the representation is centered at origin (e.g., a PolygonalFaceSet unit cube). The previous fix (24f7629f) removed translate_obj_to_z_location to prevent doubling Z for extrusion representations, but that broke centered ones where z != location.z.

Replace the removed relative translate with an absolute active_obj.location.z = z. This is a no-op for extrusion representations (z == location.z) and corrects the position for centered ones.

Add test_regenerate_space_from_centered_cube_representation to cover the regeneration path with a centered mesh representation.

Generated with the assistance of an AI coding tool.
2026-08-02 02:12:43 +02:00
CyrilWaechter 2ad4ae8de5 Fix pyright possibly-missing-submodule in covering test
Add explicit import bonsai.core.tool to satisfy pyright's type checker,
which requires submodules to be explicitly imported rather than relying
on transitive imports from import bonsai.
2026-08-02 00:56:27 +02:00
CyrilWaechter 888595b142 Fix ruff import-ordering in covering and spatial tests
ruff check flagged unsorted imports in test/core/test_covering.py
and test/tool/test_spatial.py. Fix by reorganising import blocks.
2026-08-02 00:56:27 +02:00
CyrilWaechter ebad18d0b3 Rework TestGenerateSpace to use IFC representations instead of Blender cubes
All 8 space-generation tests now create IFC walls/slabs with real
solid-block representations using IfcExtrudedAreaSolid, instead of
relying on the old Blender-mesh bisection path (broken since 79ee88da5
switched to IFC-geometry-only for boundary detection).

- _BlockHelper provides create_wall (10x10xheight block) and create_slab
  (12x12x1.0 block) helpers using standard IfcOpenShell API calls.
- The wall block bisects to a 10x10 polygon at the cutting plane
  (matching the old cube-behaviour), and auto-height detects wall_top_z.
- Pre-existing height assertions (z=10) now pass correctly because
  auto-height = wall_top_z - base_z = 10 - 0 = 10 (the old values were
  wrong for the Blender path where h defaulted to 3).
- test_regenerate_after_wall_height_change modifies the IFC extrusion
  depth directly and bumps the geom cache token via
  _bump_geom_cache_token() instead of relying on Blender depsgraph.
- No Blender cubes are created except when absolutely needed for
  selection/active-object flow (regeneration, apply-height).
- Added ifcopenshell.util.representation to imports.
- Import _bump_geom_cache_token from bonsai.tool.spatial.

Generated with the assistance of an AI coding tool.
2026-08-02 00:56:27 +02:00
CyrilWaechter 0808bcd9c9 Add covering core tests verifying tuple-unpack fix
Covers all three covering operators with success and error-path tests
using the Prophecy mocking framework. The key assertion verifies that
get_space_polygon_from_context_visible_objects' return value is unpacked
so the polygon (not the tuple of polygon+bounding_elements) reaches
set_covering_representation_from_polygon.

Shapely geometry objects are not JSON-serialisable (Prophecy call
serialisation), so we use the plain integer 42 as a stand-in for the
polygon value.

Generated with the assistance of an AI coding tool.
2026-08-02 00:56:27 +02:00
CyrilWaechter 53084ca94c Fix covering operators to unpack tuple return from get_space_polygon_from_context_visible_objects
Three covering core functions (add_instance_flooring_covering_from_cursor,
add_instance_ceiling_covering_from_cursor, regen_selected_covering_object)
used the old single-value assignment from
get_space_polygon_from_context_visible_objects, which now returns a
(polygon, bounding_walls) tuple. The isinstance(str) guard never fired,
causing the tuple to flow into set_covering_representation_from_polygon
and raise a shapely error.

Fix by unpacking space_polygon, _ at all three call sites.

Generated with the assistance of an AI coding tool.
2026-08-02 00:56:27 +02:00
CyrilWaechter 3e6460eceb Extract space generation algorithms to ifcopenshell.util
Move Blender-independent space generation algorithms from Bonsai
(GPL) to ifcopenshell.util (LGPL):

- ifcopenshell.util.shape.bisect_mesh_plane_vf: vectorized numpy
  triangle/plane intersection for mesh bisection
- ifcopenshell.util.element.iter_top_connections: walker for
  IfcRelConnectsElements(TOP) relationships
- ifcopenshell.util.space: new module with get_boundary_lines,
  get_space_polygon, get_auto_space_height and height detection
  helpers — all operating on IFC geometry without Blender

Bonsai's tool/spatial.py now delegates to these utilities via
thin wrappers, keeping only Blender-specific concerns (cache
management with depsgraph invalidation, UI property reads).

tool/wall.py iter_wall_slab_connections delegates to
ifcopenshell.util.element.iter_top_connections.

Added 22 tests: 6 for bisect_mesh_plane_vf, 10 for space
generation algorithms, 4 for iter_top_connections, 2 Bonsai
integration tests for cache behavior.

Generated with the assistance of an AI coding tool.
2026-08-02 00:56:27 +02:00
CyrilWaechter 2f7af12734 Add auto-detect space height from elements above
Space height is now auto-detected using IFC geometry directly
(ifcopenshell.geom.create_shape + get_shape_bottom/top_elevation)
instead of Blender object bounding boxes. This fixes height detection
when the slab above is not loaded in Blender.

Detection priority:
1. IfcRelConnectsElements(TOP) connections on bounding walls
2. IfcSlab / IfcRoof elements above with XY overlap to space polygon
3. Minimum wall top Z of bounding walls
4. Fallback to space_height property (default 3m)

Added space_height and force_space_height properties to
BIMSpatialDecompositionProperties. The height field is synced to
the active space's height via active_object_callback (msgbus), not
in draw().

Added ApplySpaceHeightToSelection operator to modify
IfcExtrudedAreaSolid.Depth in place without regenerating footprint.

bounding_walls changed from list[tuple[element, obj]] to
list[entity_instance] since Blender objects are no longer needed.

Generated with the assistance of an AI coding tool.
2026-08-02 00:56:26 +02:00
CyrilWaechter d5e6c19f2b Add copy attribute to selection for boundaries
Add a paste button to IfcRelSpaceBoundary specific attributes
(RelatingSpace, RelatedBuildingElement, ParentBoundary,
CorrespondingBoundary, PhysicalOrVirtualBoundary,
InternalOrExternalBoundary) reusing the existing
copy_attribute_to_selection core function.

The core function value type hint is broadened from Union[str, None]
to Any since boundary relation attributes pass IFC entity instances.

Generated with the assistance of an AI coding tool.
2026-08-02 00:55:26 +02:00
Thomas Krijnen 064ce00826 import Any 2026-08-01 14:16:12 +02:00
Thomas Krijnen 98ce2a1b46 Move __eq__ impl back to the mixin and fix test_rules.py test 2026-08-01 13:16:51 +02:00
Thomas Krijnen 6abeb459a2 Mark v0.8.0 as merged 2026-08-01 10:50:58 +02:00
dependabot[bot] c4d402eb21 build(deps): bump actions/setup-python from 6 to 7
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 10:50:34 +02:00
Bartok 4ea05f79c3 docs: add Eigen to Linux install deps
Linux "basic dependencies" omitted libeigen3-dev even though
ifcgeom requires Eigen3 (find_package Eigen3 REQUIRED) and the
cmake snippet already passes -DEIGEN_DIR=/usr/include/eigen3.
macOS Homebrew line already installs eigen.

Closes #6903

Generated with the assistance of an AI coding tool.
2026-08-01 10:50:34 +02:00
Petru Conduraru 6b917ba36d Fix test_rules.py filtering by sys.argv, which empties the corpus under pytest
test_file's parametrize list was filtered with `sys.argv[1] in
os.path.basename(fn)`, reading the raw process argv instead of a
pytest-native option. Under a bare `pytest` invocation sys.argv[1] is
pytest's own first CLI token, never a match, so the 138-fixture EXPRESS
rule corpus in test/fixtures/rules collapses to an empty parametrize and
pytest reports it as a single skipped test rather than an error. Under
CI's actual invocation (pytest -p no:pytest-blender -n $NPROCS test ...)
sys.argv[1] is "-p", which happens to substring-match 47 of the 138
fixtures, so CI has been silently running a coincidental 34% slice of
the corpus with no signal anything was wrong.

Replaced the module-level list comprehension with a pytest_generate_tests
hook plus a --rule CLI option (added via a new test/conftest.py). This
runs the full corpus by default under any pytest invocation, still
allows filtering to one rule for local debugging via --rule, and no
longer collides with pytest's own argv.

Verified all 138 fixtures collect and pass under the fixed harness
(63 fail- fixtures each raise a violation, 75 pass- fixtures raise none).

Generated with the assistance of an AI coding tool.
2026-08-01 10:50:30 +02:00
Petru Conduraru fa1b70883f ifcmcp: pin mcp below 2.0 to fix broken FastMCP import
mcp 2.0.0 (unpinned in CI and in the ifcmcp[mcp] extra) renamed
mcp.server.fastmcp.FastMCP to mcp.server.mcpserver.MCPServer, which
ifcmcp does not support yet. server.py caught the resulting
ModuleNotFoundError with a bare except Exception and silently
reported it as FastMCP not installed, masking the real breakage
until the ifcmcp test suite failed in CI.

Pinned mcp to >=1.0,<2 in both ci.yml and ifcmcp's pyproject.toml
mcp extra, confirmed the full ifcmcp test suite (70 tests) passes
against mcp 1.29.0, and confirmed the genuinely-not-installed path
still raises the expected ImportError. Also narrowed the except
clause to ImportError only so an unrelated future bug in that
import block surfaces instead of being swallowed as "not installed".

Generated with the assistance of an AI coding tool.
2026-08-01 10:49:17 +02:00
Petru Conduraru 913f9f2262 ifcopenshell.template: fix timestring ignoring an explicit timestamp of 0
create(timestamp=0) computed the FILE_NAME timestring with
`d.get("timestamp") or time.time()`, which treats 0 (a legitimate
epoch timestamp) as unset because 0 is falsy. The header ended up
with the current wall-clock time in FILE_NAME while IFCOWNERHISTORY
correctly stored CreationDate=0, an inconsistent pair of dates in
the same file. Switched to an explicit None check so an explicit
timestamp of 0 is honoured the same way any other explicit
timestamp is.

Generated with the assistance of an AI coding tool.
2026-08-01 10:49:17 +02:00
yekose 34da3950e3 ifcgeom: add a profile_point overload taking a plain double
The profile mapping builds its points as

    profile_helper(m4, {
        {{-x, -y}, {f2}},
        ...

where `f2` is a `double` and profile_point's second member is a
`boost::optional<double>`. In recent Boost (somewhere between 1.85 and 1.91)
optional's converting constructor became explicit, and an explicit constructor
cannot be used in copy-initialization — which is what a braced element is. So
every one of these call sites stops compiling:

  MSVC 19.4x:  error C2664: cannot convert argument 2 from
               'initializer list' to 'const std::vector<profile_point>&'
  clang-cl 22: error: chosen constructor is explicit in copy-initialization

Twelve translation units are affected (IfcCShapeProfileDef,
IfcIShapeProfileDef, IfcLShapeProfileDef, IfcTShapeProfileDef,
IfcUShapeProfileDef, IfcZShapeProfileDef, IfcAsymmetricIShapeProfileDef,
IfcCraneRailAShapeProfileDef, IfcRectangleProfileDef,
IfcRectangleHollowProfileDef, IfcRoundedRectangleProfileDef,
IfcTrapeziumProfileDef), roughly 100 call sites in total.

Adding one overload that takes the double directly fixes all of them without
touching a single call site, and changes nothing for existing code: the
optional overload still wins wherever an optional is passed.

Verified by building schemas 2x3;4;4x3_add2 with MSVC 2022 against Boost
1.91 and OCCT 7.9.3 — IfcParse, IfcGeom, the schema mappings and
geometry_kernel_opencascade all archive cleanly. Without this, the same build
against Boost 1.85 succeeds, which is what identified Boost as the variable.
2026-08-01 10:49:17 +02:00
CyrilWaechter 24f7629fad Fix space regen doubling Z location
Removing translate_obj_to_z_location from the existing-IfcSpace
regeneration branch. The ShapeBuilder rewrite (d8de62308) builds
geometry in local space preserving obj.matrix_world, making the
translate call redundant — it adds z on top of the already-correct
location.z, producing 2*z.

Add test_regenerate_space_preserves_z_location to cover the
regeneration path with a non-zero Z elevation.

Generated with the assistance of an AI coding tool.
2026-08-01 10:49:17 +02:00
dependabot[bot] a11ebdf8c4 build(deps): bump actions/setup-python from 6 to 7
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-31 13:57:34 +02:00
Bartok b997726564 docs: add Eigen to Linux install deps
Linux "basic dependencies" omitted libeigen3-dev even though
ifcgeom requires Eigen3 (find_package Eigen3 REQUIRED) and the
cmake snippet already passes -DEIGEN_DIR=/usr/include/eigen3.
macOS Homebrew line already installs eigen.

Closes #6903

Generated with the assistance of an AI coding tool.
2026-07-31 12:49:37 +02:00
Petru Conduraru 25713a486a Fix test_rules.py filtering by sys.argv, which empties the corpus under pytest
test_file's parametrize list was filtered with `sys.argv[1] in
os.path.basename(fn)`, reading the raw process argv instead of a
pytest-native option. Under a bare `pytest` invocation sys.argv[1] is
pytest's own first CLI token, never a match, so the 138-fixture EXPRESS
rule corpus in test/fixtures/rules collapses to an empty parametrize and
pytest reports it as a single skipped test rather than an error. Under
CI's actual invocation (pytest -p no:pytest-blender -n $NPROCS test ...)
sys.argv[1] is "-p", which happens to substring-match 47 of the 138
fixtures, so CI has been silently running a coincidental 34% slice of
the corpus with no signal anything was wrong.

Replaced the module-level list comprehension with a pytest_generate_tests
hook plus a --rule CLI option (added via a new test/conftest.py). This
runs the full corpus by default under any pytest invocation, still
allows filtering to one rule for local debugging via --rule, and no
longer collides with pytest's own argv.

Verified all 138 fixtures collect and pass under the fixed harness
(63 fail- fixtures each raise a violation, 75 pass- fixtures raise none).

Generated with the assistance of an AI coding tool.
2026-07-31 10:38:58 +02:00
Petru Conduraru d3b6b82151 ifcmcp: pin mcp below 2.0 to fix broken FastMCP import
mcp 2.0.0 (unpinned in CI and in the ifcmcp[mcp] extra) renamed
mcp.server.fastmcp.FastMCP to mcp.server.mcpserver.MCPServer, which
ifcmcp does not support yet. server.py caught the resulting
ModuleNotFoundError with a bare except Exception and silently
reported it as FastMCP not installed, masking the real breakage
until the ifcmcp test suite failed in CI.

Pinned mcp to >=1.0,<2 in both ci.yml and ifcmcp's pyproject.toml
mcp extra, confirmed the full ifcmcp test suite (70 tests) passes
against mcp 1.29.0, and confirmed the genuinely-not-installed path
still raises the expected ImportError. Also narrowed the except
clause to ImportError only so an unrelated future bug in that
import block surfaces instead of being swallowed as "not installed".

Generated with the assistance of an AI coding tool.
2026-07-31 10:36:39 +02:00
yekose 9e6797e172 ifcparse: check the result of fopen before using the FILE*
FullBufferImpl and PagedFileImpl both open the file and then use the handle
without ever testing it:

    auto stream = _wfopen(fn_wide, L"rb");   // null when the file is missing
    fseek(stream, 0, SEEK_END);              // null goes straight to the CRT
    buf_.resize((size_t)ftell(stream));

Opening a path that does not exist therefore hands a null FILE* to the CRT. On
MSVC that does not return an error: the runtime terminates the process
immediately (fastfail, exit code 0xC0000409). No exception is thrown, no stack
unwinding starts, so a caller cannot defend with try/catch — the host
application simply dies. On glibc it is undefined behaviour as well.

This is reachable through the ordinary entry point, because guess_file_type()
answers FT_IFCSPF for a path that does not exist (its own comment calls this
"just weird, but for consistency with earlier behaviour"), so a missing path
flows into the reader rather than being reported.

The fix is to leave the reader empty when the open fails. Both implementations
then behave like a zero-length file: size() is 0 and get() throws out_of_range
for any position, so the parse fails and IfcFile::good() reports it, which is
what a caller can actually handle. PagedFileImpl's destructor already tested
fp_ for null, so the possibility was known — only the constructor did not check.

Verified by reading a non-existent path through IfcParse::IfcFile: the
constructor returns and good() reports the failure, where before the process
died with 0xC0000409 and no output.
2026-07-31 10:33:04 +02:00
Petru Conduraru 1f9a0a53bb ifcopenshell.template: fix timestring ignoring an explicit timestamp of 0
create(timestamp=0) computed the FILE_NAME timestring with
`d.get("timestamp") or time.time()`, which treats 0 (a legitimate
epoch timestamp) as unset because 0 is falsy. The header ended up
with the current wall-clock time in FILE_NAME while IFCOWNERHISTORY
correctly stored CreationDate=0, an inconsistent pair of dates in
the same file. Switched to an explicit None check so an explicit
timestamp of 0 is honoured the same way any other explicit
timestamp is.

Generated with the assistance of an AI coding tool.
2026-07-31 10:19:33 +02:00
yekose b82c4c53fe ifcgeom: add a profile_point overload taking a plain double
The profile mapping builds its points as

    profile_helper(m4, {
        {{-x, -y}, {f2}},
        ...

where `f2` is a `double` and profile_point's second member is a
`boost::optional<double>`. In recent Boost (somewhere between 1.85 and 1.91)
optional's converting constructor became explicit, and an explicit constructor
cannot be used in copy-initialization — which is what a braced element is. So
every one of these call sites stops compiling:

  MSVC 19.4x:  error C2664: cannot convert argument 2 from
               'initializer list' to 'const std::vector<profile_point>&'
  clang-cl 22: error: chosen constructor is explicit in copy-initialization

Twelve translation units are affected (IfcCShapeProfileDef,
IfcIShapeProfileDef, IfcLShapeProfileDef, IfcTShapeProfileDef,
IfcUShapeProfileDef, IfcZShapeProfileDef, IfcAsymmetricIShapeProfileDef,
IfcCraneRailAShapeProfileDef, IfcRectangleProfileDef,
IfcRectangleHollowProfileDef, IfcRoundedRectangleProfileDef,
IfcTrapeziumProfileDef), roughly 100 call sites in total.

Adding one overload that takes the double directly fixes all of them without
touching a single call site, and changes nothing for existing code: the
optional overload still wins wherever an optional is passed.

Verified by building schemas 2x3;4;4x3_add2 with MSVC 2022 against Boost
1.91 and OCCT 7.9.3 — IfcParse, IfcGeom, the schema mappings and
geometry_kernel_opencascade all archive cleanly. Without this, the same build
against Boost 1.85 succeeds, which is what identified Boost as the variable.
2026-07-31 10:14:16 +02:00
Thomas Krijnen d3ca116534 Even more plug-in workarounds 2026-07-31 07:47:34 +02:00
Thomas Krijnen 3d1e157ff1 Even more plug-in workarounds 2026-07-31 07:17:36 +02:00
Thomas Krijnen 8d5cedd152 Remove duplicate install 2026-07-31 07:17:07 +02:00
Thomas Krijnen fb17c66bf4 cmake rpath and interface fixes 2026-07-31 05:55:20 +02:00
Thomas Krijnen 008ac8a354 Discard old examples - modernize others 2026-07-31 03:58:50 +02:00
Dion Moult 3fc83847dd Get SQL is_a() working again 2026-07-30 21:56:33 +10:00
Dion Moult ea5c8343c2 If we have a global git push setting, then config_push won't be setup for our IFC repo 2026-07-30 21:43:18 +10:00
Dion Moult e033a4471a Fix typo and scale setting is std::string 2026-07-30 21:19:28 +10:00
Thomas Krijnen 41308cf122 Cmake install config fixes 2026-07-30 08:54:08 +02:00
Thomas Krijnen 6d7aa3c48c translate ../bin -> ../lib search path for plug-ins for installed execs 2026-07-30 03:35:21 +02:00
Thomas Krijnen 4b14c21963 schema nullptr dereference 2026-07-30 03:34:46 +02:00
Thomas Krijnen d472814e2a Don't try and set empty aggregates (todo: see if they are still needed with the type-aware upgrade-based parse mode) 2026-07-30 03:16:39 +02:00
Thomas Krijnen e033729233 Try rpath fix for shared build 2026-07-30 02:42:54 +02:00
Thomas Krijnen 5431b60508 Use matrix in ci.yml for BUILD_SHARED_LIBS 2026-07-30 02:42:39 +02:00
Dion Moult a4f2075c3d ifcviewer: x-ray marquee selects through occluders
Box select resolved hits by reading the depth-tested object_id MRT, so only
the front-most surface in each pixel could ever come back. In x-ray that is
wrong twice over: you can see the geometry behind, and you still cannot
select it.

Add a second box-pick path used only while x-ray is active. It runs the same
vs_pick geometry through fs_boxpick with depth compare Always, no depth write
and no colour targets, scissored to the marquee — so nothing culls a fragment
behind another and the pass's only output is an atomicOr of one bit per
object into a hit bitmask. Reading that back gives every object with geometry
inside the box, occluded or not.

The bitmask rides alongside sel_flags at group(0) binding 2, allocated and
bound by ensureSelectionFlagsBuffer so the two can never disagree about how
many object ids exist. The layout entry is FRAGMENT-visible only: WebGPU
forbids a read_write storage buffer in the vertex stage, and every pipeline
shares this layout. Back-face culling is off for the pass — a box landing
inside a closed solid would otherwise see none of its faces and miss it.

Outside x-ray the depth-tested read stands, so a plain marquee still takes
only what is visible. A failure to build the pipeline falls back to that path
rather than breaking box select.

Tests cover the three properties worth having: x-ray selects strictly more,
its result is a superset of the plain one (a bare count would wave through a
wrong scissor or an off-by-one in the bit decode), and turning x-ray off
restores front-most-only. They need a model with real self-occlusion, which
sidecar_bake cannot currently produce — it segfaults on any input, including
the pristine sample.ifc — so they skip with an explanation until a fixture is
supplied. See the note at the top of the spec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 07:46:22 +10:00
Dion Moult 8cc96127ce ifcviewer-web: let host pages choose the mouse nav preset
ViewportCore has had a preset table (blender / rhino / revit / web) since the
nav bindings were shared with the desktop, and the web input handlers already
classify presses against it — but main() hard-coded "web" and nothing could
reach setNavPreset from JS, so every page was stuck on LMB-orbit.

Export ifcv_set_nav_preset_c and wrap it as IfcViewer.create's `navPreset`
option plus a setNavPreset() method. The preset is only the button/modifier
table, so it needs no GPU state: it applies as soon as the module resolves,
which means the first drag already uses the host's scheme rather than
flipping after a frame or two.

Names are validated in JS against the four the core knows. The core silently
falls back to blender for anything unrecognised, which would turn a typo into
a mystery change of scheme rather than an error.

The default stays "web", so existing pages and the smoke tests are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 07:46:22 +10:00
Thomas Krijnen 6bcfb175a3 Bring back create_entity() based on global file dict 2026-07-29 13:48:18 +02:00
Thomas Krijnen ba8dc53718 Fix streamer header ownership
Create file-owned headers after storage is selected but before streaming starts. Let owner-backed streamers use that header directly, and keep owned_header_ exclusively for ownerless streamers.

Generated with the assistance of an AI coding tool.
2026-07-28 04:19:28 +02:00
Thomas Krijnen 02bc09029e spf_header::assign() cfr a9d67768 2026-07-28 04:19:01 +02:00
Thomas Krijnen 9c965e9f08 Set IfcConvert install RPATH
Configure the executable to find installed shared libraries without relying on the build-tree runtime paths.
2026-07-28 04:19:01 +02:00
Thomas Krijnen e122f2cec3 Defer header initialization to aafter storage is set - cfr dcebf23a 2026-07-28 04:18:52 +02:00
Dion Moult 5739a24808 CI: make the Autodesk connector workflow a test gate, ship it on macOS
Three related cleanups to how the bonsaiviewer-autodesk connector is built
and shipped.

build-bonsaiviewer-autodesk.yml no longer builds a bundle. Its four-runner
matrix produced autodesk-<os>-<arch>.zip artifacts that nothing consumed —
shipping happens in the platform pipelines, which each invoke
packaging/build.py themselves. What is left is the crate's only lint and
test coverage, so the workflow is renamed to match what it does and a
header comment records where the shipped binary actually comes from.

build_osx.yml now builds and bundles the connector, which it never did:
macOS users have been getting a Bonsai Viewer with no Autodesk connector at
all. ConnectorDiscovery resolves applicationDirPath()/connectors, which
inside a bundle is Contents/MacOS, so that is where the folder lands.

The tkinter probes in the Windows and Linux workflows are dropped. They
guarded the old PyInstaller connector's Tk GUI (de7520418) and have been
dead since the Rust rewrite (9d9f4054f); python3.11-tkinter goes with them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:43:28 +10:00
Dion Moult 0aef201d50 bonsaiviewer-autodesk: satisfy cargo fmt and clippy
The crate had never been run through rustfmt, so the CI job's first step
(`cargo fmt --all -- --check`) failed and masked 12 clippy errors behind
it. Fix both.

Beyond the mechanical reformat and the redundant-closure/div_ceil/Default
lints, two changes carry meaning:

- SettingsDialog::on_reload is UI-thread only, so it becomes an Rc. The
  Arc was never shared across threads and clippy rightly flagged it as
  an Arc over a non-Send/Sync closure.
- WorkerMsg variants lose their shared `Loaded` postfix; the enum's doc
  comment already says these are worker completions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:39:21 +10:00
Dion Moult a84957fd45 Fix Wrong number or type of arguments for overloaded function 'new_file
I got this error, so I used the same pattern I saw in the generated wrapper py:

                            |   File "/home/dion/.config/blender/5.2/extensions/.local/lib/python3.13/site-packages/ifcopenshell/api/project/create_file.py", line 54, in create_file
                            |     file = ifcopenshell.file(schema=version)
                            |   File "/home/dion/.config/blender/5.2/extensions/.local/lib/python3.13/site-packages/ifcopenshell/ifcopenshell_wrapper.py", line 5242, in __init__
                            |     _ifcopenshell_wrapper.new_file(self, identifier)
                            |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
                            | TypeError: Wrong number or type of arguments for overloaded function 'new_file'.
                            |   Possible C/C++ prototypes are:
                            |     ifcopenshell::file::file(std::string const &,ifcopenshell::filetype,bool,::logger &)
                            |     ifcopenshell::file::file(std::string const &,ifcopenshell::filetype,bool)
                            |     ifcopenshell::file::file(std::string const &,ifcopenshell::filetype)
                            |     ifcopenshell::file::file(std::istream &,int,::logger &)
                            |     ifcopenshell::file::file(std::istream &,int)
                            |     ifcopenshell::file::file(void *,int,::logger &)
                            |     ifcopenshell::file::file(void *,int)
                            |     ifcopenshell::file::file(ifcopenshell::schema_definition const *,ifcopenshell::filetype,std::string const &,::logger &)
                            |     ifcopenshell::file::file(ifcopenshell::schema_definition const *,ifcopenshell::filetype,std::string const &)
                            |     ifcopenshell::file::file(ifcopenshell::schema_definition const *,ifcopenshell::filetype)
                            |     ifcopenshell::file::file(ifcopenshell::schema_definition const *)
                            |     ifcopenshell::file::file()
                            |     ifcopenshell::file::file(ifcopenshell::uninitialized_tag const &,::logger &)
                            |     ifcopenshell::file::file(ifcopenshell::uninitialized_tag const &)
                            |     ifcopenshell::file::file(std::string const &)
2026-07-27 19:22:15 +10:00
Dion Moult 19a1d88970 Fix all ty diagnostics on ifcviewer-wgpu (ci-lint ty-ios + ty-bonsai)
This branch carried v0.8.0's strict `[tool.ty.rules] all = "error"` config but
not the source fixes that were made upstream to satisfy it, so both ci-lint ty
gates were failing: `poe ty-ios` reported 256 diagnostics and `poe ty-bonsai`
258. Both are now clean.

Most fixes are ported from v0.8.0 and follow two idioms: initialise a name
before a conditional that may not bind it (plus an `assert` where the invariant
is real but not provable), and close an exhaustive `if`/`elif` chain with
`else: assert False, <discriminant>`.

The branch's own newer accessors are preserved throughout - `.file`,
`.declaration`, `file.types()`, `get_max_id()` are kept rather than reverted to
`wrapped_data.*`, and non-ty upstream changes (notably the in-progress geometry
cache removal) are deliberately not pulled in.

Notable fixes that are not straight ports:

* ifcopenshell_wrapper.pyi: `entity_instance.file` was declared as
  `def file(self) -> file`, where the property name shadows the `class file`
  below it, so the annotation resolved to `Unknown`. Every `element.file` in
  the codebase was therefore unchecked. Qualifying it to `ifcopenshell.file`
  restores `.schema` to its Literal union and surfaces no new diagnostics.

* model/wall.py: a duplicated merge fragment in the void-straddle path ran an
  always-true `if void_straddles:` that read `new_opening` from the mutually
  exclusive branch (stale value, or NameError on the first iteration), followed
  by an unreachable duplicate `elif`. Removing it makes the file match v0.8.0.

* light/operator.py: upstream's own fix unpacks three targets from two values
  and raises ValueError unconditionally; corrected to `None, None, None`.

* assign_system.py, validate.py, geom/main.py: walrus-in-genexp is valid at
  runtime (PEP 572 binds in the containing scope) but ty does not model it;
  rewritten as explicit loops, matching upstream.

Verified: poe ty-ios, poe ty-bonsai, ruff check src/ nix/, black --check .,
and compileall -W error at py3.10 (ifcopenshell-python) and py3.11 (bonsai).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 19:14:39 +10:00
Dion Moult a522f31ba4 Bump swig version to 4.2.1 in ci.yml to be consistent with build-all.py 2026-07-27 09:28:20 +10:00
CyrilWaechter 8deefe497c Fix space regen doubling Z location
Removing translate_obj_to_z_location from the existing-IfcSpace
regeneration branch. The ShapeBuilder rewrite (d8de62308) builds
geometry in local space preserving obj.matrix_world, making the
translate call redundant — it adds z on top of the already-correct
location.z, producing 2*z.

Add test_regenerate_space_preserves_z_location to cover the
regeneration path with a non-zero Z elevation.

Generated with the assistance of an AI coding tool.
2026-07-26 16:19:51 +02:00
Dion Moult f0c0312e8d Fix Windows (MSVC) and WASM (Emscripten) build failures
Both surfaced on the first Windows/WASM CI run of this branch:

- XmlSerializer.cpp: the IfcPropertySetDefinitionSet block used a C-style
  cast to convert the set to std::vector<IfcPropertySetDefinition>. GCC
  invokes the non-explicit conversion operator; MSVC rejects the cast to a
  template type (C2440/C3536/C2661). Use copy-initialisation instead, which
  invokes the same implicit conversion portably. (This block was dead until
  the SCHEMAS_->SCHEMA_HAS_ typo fix enabled it, so it had never hit MSVC.)

- parse.cpp: the floating-point parse path falls back to strtod_l because
  libc++ =deletes the float from_chars overload. That fallback was guarded
  for __APPLE__ only; Emscripten uses the same libc++, so WASM hit the
  deleted from_chars. Extend the guard to __EMSCRIPTEN__ (its musl provides
  strtod_l/newlocale, treating all locales as C).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 19:37:11 +10:00
Dion Moult c9c3beb139 poe ruff 2026-07-26 18:05:39 +10:00
Dion Moult 62fa2cad01 Remove my local build.sh 2026-07-26 18:05:35 +10:00
Dion Moult 291d7d8441 black . 2026-07-26 18:03:09 +10:00
Dion Moult 7aa967b01a ifcparse: recover two fixes lost in the datamodel-rewrite merge
Both fixes were made on v0.8.0's IfcParse.cpp AFTER the datamodel branch
had already renamed it to parse.cpp. When main was later merged into the
branch, the modify/delete was resolved toward the deletion (merge
8c9c3cde2), so the changes never reached the live parse.cpp. They are
invisible to `git log v0.8.0...HEAD` because the originating commits sit
in the merged-in shared ancestry; only a content sweep of the renamed
files surfaces them.

- format_double now uses the shortest decimal representation that
  round-trips exactly (Mac-safe manual implementation, no std::to_chars),
  instead of setprecision(digits10) which padded clean REALs with noise
  digits (0.0174532925199433 -> 0.017453292519943299) and rewrote every
  REAL on re-save. Recovers ee2b357d7 + fa597536e + 821cf7b67, #7696.
- The [SYN004] non-entity-type parse-error branch now resets current_id
  to 0 before advancing, matching its sibling error branches, so a
  malformed non-entity instance no longer erroneously terminates parsing.
  Recovers 7c9df9f98.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 16:32:47 +10:00
Dion Moult 983707cfec Reconcile ty config with v0.8.0: adopt all=error rules, pin ty 0.0.63
During the v0.8.0->wgpu port replay, ty config changes in pyproject.toml were
deferred (wgpu's whitelist all=ignore kept, v0.8.0's code fixes applied). Now
adopt v0.8.0's stricter blacklist config (all=error with curated ignores) and
pin ty to 0.0.63 to match. The pyproject diff was ty-only, so no wgpu-specific
config is lost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:51:19 +10:00
Petru Conduraru 7a82c9b0a3 ifcpatch: correct the AGS2IFC docstring example
The example block was copy pasted verbatim from ExtractPropertiesToSQLite,
so it named the wrong recipe and wrote a .sqlite file. These docstrings are
what ifcpatch surfaces as CLI and UI help, so anyone following the example
for AGS2IFC got a recipe name that does not match the one they selected.

Also state that the input file is not read and that a new IFC4X3 model is
built, since that is not obvious from the signature and the recipe creates
its own project rather than patching the one passed in.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 9621388953)
2026-07-25 23:50:14 +10:00
Dion Moult 55374d78ed svgfill: use std::optional instead of transitive boost::optional
Graph2D::query and arrange_polygons relied on boost::optional reaching
them transitively through CGAL/boost headers. That transitive include no
longer happens on newer toolchains (GCC 14 / newer libstdc++), so the
build breaks with "boost::optional does not name a template type". The
rest of the svgfill module already uses std::optional; migrate these two
holdouts to match and include <optional> explicitly rather than freeload
on a fragile transitive include.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:27:11 +10:00
Petru Conduraru 0b7262215e Bonsai: fix UnboundLocalError crash in polyline angle calculation
angle_round_threshold was only assigned inside the `distance > 0`
branch of calculate_distance_and_angle, but read unconditionally
whenever should_round is True. When the mouse sample coincides with
the last placed point (distance == 0), such as the first mouse move
after placing a wall's start point on a YZ plane view, this crashed
the modal wall tool.

angle_round_threshold is a fixed cutoff unrelated to whether distance
is currently zero, so it is now assigned once before the branch.

Fixes #8597.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 89523999b3)
2026-07-25 23:20:00 +10:00
Petru Conduraru 561826778b Fix ci-bonsai-daily: configure unmerged_blobs mock in git_mergetool tests (#8574)
test_returns_none_when_report_file_absent/empty build a MagicMock repo
without configuring index.unmerged_blobs(), so it returned a truthy
MagicMock and git_mergetool's load-bearing "unresolved conflicts remain"
fallback (tool/ifcgit.py:646-647) returned that list instead of None -
failing "assert [] is None". The production fallback is correct and
intentionally left untouched; the tests just misrepresented the
"mergetool resolved cleanly" scenario they are named for. Set
mock_repo.index.unmerged_blobs.return_value = {} in both.

Verified in headless Blender: test/tool/test_ifcgit.py::TestGitMergetool
2 failed / 1 passed -> 3 passed.

This change was made with the assistance of an AI tool.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 51ab38de27)
2026-07-25 23:18:33 +10:00
Petru Conduraru 82efde5741 ifcdiff: fix crash when exporting property diffs to JSON
DeepDiff's dictionary_item_added/set_item_added results are a
deepdiff.helper.SetOrdered instance, which subclasses orderly_set's
StableSetEq rather than the OrderedSet class json_dump_default checked
for, so the property relationship check always crashed export() with
"Object of type SetOrdered is not JSON serializable". Check against
StableSet, the common base class shared by every orderly_set set
flavour, instead.

Fixes #8905

Generated with the assistance of an AI coding tool.

(cherry picked from commit fbe36532a0)
2026-07-25 23:18:33 +10:00
Andrej730 3f2fbcd12d ifcwrap: use swig shadowing for keeping reference to Element
(cherry picked from commit 2f1b2f9638)
2026-07-25 23:18:33 +10:00
Andrej730 6c171ca9e4 ifcwrap: fix breaking validate_stub (824c1fc)
It's ignoring underscore prefixed functions as not actually used.
Removing underscore to keep it happy without adding new exceptions.

(cherry picked from commit 88c8bd032f)
2026-07-25 23:18:33 +10:00
Andrej730 08d3d99b1e ifcwrap: ignore newly added conversion settings structs (183e4c4)
(cherry picked from commit 3d8654acfd)
2026-07-25 23:18:33 +10:00
Andrej730 30bcea3224 ci: fix failing test for ifc5d
(cherry picked from commit b14df627d7)
2026-07-25 23:18:33 +10:00
Andrej730 83aef5e4d5 black .
(cherry picked from commit a586c7f695)
2026-07-25 23:18:33 +10:00
Andrej730 b37499ddf7 ty: add ignores
(cherry picked from commit 0bad5a9389)
2026-07-25 23:18:33 +10:00
dependabot[bot] 200c97f89b build(deps): bump gersemi from 0.26.1 to 0.28.0
Bumps [gersemi](https://github.com/BlankSpruce/gersemi) from 0.26.1 to 0.28.0.
- [Release notes](https://github.com/BlankSpruce/gersemi/releases)
- [Changelog](https://github.com/BlankSpruce/gersemi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BlankSpruce/gersemi/compare/0.26.1...0.28.0)

---
updated-dependencies:
- dependency-name: gersemi
  dependency-version: 0.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 0ce400cace)
2026-07-25 23:18:33 +10:00
dependabot[bot] 96e2b3fa57 build(deps): bump ruff from 0.15.22 to 0.16.0
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.22 to 0.16.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.22...0.16.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 91ed59311b)
2026-07-25 23:18:33 +10:00
dependabot[bot] 102e08efe5 Bump svelte from 5.53.6 to 5.55.8 in /src/ifctester/webapp
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.53.6 to 5.55.8.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.55.8/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.55.8
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 1906481a01)
2026-07-25 23:18:33 +10:00
dependabot[bot] e6e80246f1 Bump uuid and hyperid in /src/ifctester/webapp
Removes [uuid](https://github.com/uuidjs/uuid). It's no longer used after updating ancestor dependency [hyperid](https://github.com/mcollina/hyperid). These dependencies need to be updated together.

Removes `uuid`

Updates `hyperid` from 3.3.0 to 4.0.0
- [Release notes](https://github.com/mcollina/hyperid/releases)
- [Commits](https://github.com/mcollina/hyperid/compare/v3.3.0...v4.0.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version:
  dependency-type: indirect
- dependency-name: hyperid
  dependency-version: 4.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 73bf238232)
2026-07-25 23:18:33 +10:00
dependabot[bot] 695e91ef1a Bump lxml from 4.9.1 to 6.1.0 in /src/ifcopenshell-python
Bumps [lxml](https://github.com/lxml/lxml) from 4.9.1 to 6.1.0.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.9.1...lxml-6.1.0)

---
updated-dependencies:
- dependency-name: lxml
  dependency-version: 6.1.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit a85d5cc990)
2026-07-25 23:18:33 +10:00
dependabot[bot] 887cd35fa5 Bump ws and engine.io-client in /src/ifctester/webapp
Bumps [ws](https://github.com/websockets/ws) and [engine.io-client](https://github.com/socketio/socket.io). These dependencies needed to be updated together.

Updates `ws` from 8.17.1 to 8.21.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.17.1...8.21.0)

Updates `engine.io-client` from 6.6.3 to 6.6.6
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/engine.io-client@6.6.3...engine.io-client@6.6.6)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.21.0
  dependency-type: indirect
- dependency-name: engine.io-client
  dependency-version: 6.6.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit c16aec2cb0)
2026-07-25 23:18:33 +10:00
dependabot[bot] 04d70d4a2a build(deps-dev): bump immutable in /src/ifctester/webapp
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 5.1.5 to 5.1.9.
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v5.1.5...v5.1.9)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 5.1.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 76be31561e)
2026-07-25 23:18:33 +10:00
dependabot[bot] 89ae21c3ba Bump devalue from 5.6.4 to 5.8.1 in /src/ifctester/webapp
Bumps [devalue](https://github.com/sveltejs/devalue) from 5.6.4 to 5.8.1.
- [Release notes](https://github.com/sveltejs/devalue/releases)
- [Changelog](https://github.com/sveltejs/devalue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/devalue/compare/v5.6.4...v5.8.1)

---
updated-dependencies:
- dependency-name: devalue
  dependency-version: 5.8.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit a111c68d44)
2026-07-25 23:18:33 +10:00
dependabot[bot] fc97ccb6dc Bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 62f627ecc6)
2026-07-25 23:18:33 +10:00
dependabot[bot] c6b3c54fc5 build(deps-dev): bump tar from 7.5.16 to 7.5.21 in /src/ifctester/webapp
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.16 to 7.5.21.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.16...v7.5.21)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.21
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 279e16f2ab)
2026-07-25 23:18:33 +10:00
dependabot[bot] 71654a2af2 build(deps-dev): bump postcss in /src/ifctester/webapp
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.4 to 8.5.22.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.4...8.5.22)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.22
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 3d68d0f0e5)
2026-07-25 23:18:33 +10:00
Petru Conduraru f13f471364 Bonsai: cache-bust webui static assets so shipped JS/CSS changes reach users
Browsers were caching /static/js and /static/css for the standalone
webui (costing, gantt, drawings, index, demo pages) indefinitely, so a
shipped JS fix (e.g. the Download CSV button) would only reach a user
after a manual hard refresh.

Two changes, applied consistently across all five webui pages.

1. Every locally served link/script tag in the pystache templates now
carries a ?v=<bonsai version> query string, falling back to a static
asset mtime hash when BONSAI_VERSION isn't set (e.g. running
sioserver.py standalone). Since get_bonsai_version() includes the
build's commit hash, the token changes on every shipped update.

2. Responses under /static/ and /jsgantt/ now carry
Cache-Control: no-cache, must-revalidate. This covers what query
stamping alone can't reach: cost.js and gantt.js statically import
utilities/costui.js by a fixed relative path with no query string, so
that nested module still needed server side revalidation to pick up
changes.

Verified against a live aiohttp instance of sioserver.py: rendered
HTML for all five routes shows the stamped URLs, and the token
changes when BONSAI_VERSION changes between two server runs. A
conditional GET against a static file with a stale If-Modified-Since
header confirms the cheap 304 revalidation path still works.

Also used this instance plus a real headless Chromium (Playwright) to
click test the previously untested Download CSV button on the costing
page. The ribbon renders it correctly, and clicking it (with a
synthetic cost-items table injected into the DOM to stand in for a
connected Blender's data) triggers a real Blob download with the
correct filename and CSV content. No bug found, the button works as
intended.

AI-generated with Claude Code.

(cherry picked from commit e759135608)
2026-07-25 23:18:33 +10:00
Petru Conduraru e308ecaeb4 ifc5d: match cost schedule export columns to the Bonsai cost panel (#6251)
Stefano's final ask on #6251 was specific: the ODS/XLSX export should
show exactly what the cost panel shows, ID (Identification), Name,
Quantity, Value, Total Cost, no more, no less. The previous fix in
this PR removed the internal bookkeeping columns but still exported
Description, Unit and a per-category cost breakdown (Labor Cost,
Material Cost, etc), none of which appear in the panel.

Presentation formats (.ods/.xlsx) now use an explicit allow-list of
columns instead of a block-list of internal ones, and relabel headers
to match the panel's own wording (ID / Value / Total Cost). The .csv
format is unchanged: csv2ifc still reads back the extra bookkeeping
columns for the import round trip, which is why it keeps them.

Also add a "Download CSV" button to the browser costing view
(Generate spreadsheet browser), which previously only offered a
clipboard-based Copy Selected. It reuses the already-rendered table
(respecting the user's column visibility settings) and triggers a
real file download, dropping only the UI-only Actions column.

AI-generated with Claude Code; reviewed and tested by Petru Conduraru.

(cherry picked from commit 1df738d968)
2026-07-25 23:18:33 +10:00
Petru Conduraru 1d1bb2276d ifc5d: professional grade ODS/XLSX cost schedule export #6251
Three defects reported against the Costing tab export:

1. XLSX export crashed with ModuleNotFoundError: xlsxwriter was never
   bundled with Bonsai. Port the writer to openpyxl, which ifccsv
   already uses and Bonsai already ships, so it works out of the box.
2. Every ODS cell was written as a string (numbers as text), and the
   formula branch was dead code: it compared against 'Total Price' /
   'Rate Subtotal' while the headers are 'TotalPrice' / 'RateSubtotal'.
   Numeric columns are now typed float cells and TotalPrice becomes a
   real formula: Quantity*RateSubtotal on leaf items, SUM over the
   direct children's TotalPrice cells on sum items.
3. Internal bookkeeping columns (Id, ItemIsASum, Hierarchy, Index,
   Quantities) leaked into the presentation formats. ODS/XLSX now hide
   them; CSV keeps them since csv2ifc consumes them for the round trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 98c28a1f30)
2026-07-25 23:18:33 +10:00
Petru Conduraru 10028de0d7 Resolve nested complex quantity paths in the selector (#2041)
get_element_value could not reach the members of an IfcPhysicalComplexQuantity
(or IfcComplexProperty) by their natural path. util.element expands a complex
quantity into a dict whose nested members live under a "properties" sub-dict,
but the selector's dict navigation only looked at the top level, so
"Qto_Custom.Layer1.Width" returned None and IfcCsv exported nothing for it.
Only the internal "Qto_Custom.Layer1.properties.Width" path worked.

When a key is not a direct member of the value dict, descend into its
"properties" sub-dict so nested quantities/properties resolve with the
natural "Set.Complex.Nested" path. Direct keys still take priority, so the
explicit ".properties." path stays backward compatible and the regex branch
is untouched.

Verified: Qto_Custom.Layer1.Width -> 0.1 and Layer1.Height -> 2.5 (were
None), the sibling simple NetArea still resolves, the legacy .properties.
path still works, and IfcCsv now exports the nested value. test_selector.py:
38 passed (adds test_selecting_a_nested_complex_quantity).

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 21122c0d28)
2026-07-25 23:18:33 +10:00
falken10vdl af30ec412a Remove unused has_any_textures return from restore_material_style_types
(cherry picked from commit f2d8f17f88)
2026-07-25 23:18:33 +10:00
falken10vdl db71120f97 Remove unused _get_shader_label helper method
(cherry picked from commit e9b619e3fb)
2026-07-25 23:18:33 +10:00
Petru Conduraru 93d906b89b Fix ci-bonsai-daily: ProjectLibraryData duplicate parent-library enum entry (#8573)
* Fix ci-bonsai-daily: ProjectLibraryData duplicate parent-library enum

parent_libraries_enum() adds an explicit entry for get_root_context(),
then loops over cls.data["project_libraries"] (all IfcProjectLibrary
entities) and appends each. For a library-only file (no IfcProject),
get_root_context falls back to the top-level IfcProjectLibrary itself,
so the root is appended twice with the same enum key (its STEP id),
which Blender EnumProperty requires to be unique -> the data load
asserts. Normal project files are unaffected (root is an IfcProject
whose id never collides with a library id).

Skip library_id == root.id() in the loop (dedup by id, the colliding
key). Verified in headless Blender:
test_project_library_data.py::TestLibraryOnlyFile goes from 1 failed /
5 passed to 6 passed.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Bonsai: repair library files missing the required IfcProject, not just the symptom

Per the IFC Project Context concept template, every project data set (library
files included) shall contain exactly one IfcProject, and IfcProjectLibrary
instances are assigned to it via IfcRelDeclares. There is no such thing as a
spec-valid file rooted on IfcProjectLibrary alone.

get_root_context() (added in 260a387069, #8184) treated a missing IfcProject
as license to use the top-level IfcProjectLibrary as the file's root context
instead. That invalid premise is why project_libraries() (which walks every
IfcProjectLibrary, root included) then re-added that same entity, producing
the duplicate, colliding enum key this PR originally papered over with a
dedup guard.

Add tool.Project.ensure_project_context(), which repairs a file missing
IfcProject by creating one and declaring the file's root-level
IfcProjectLibrary instances to it, and tool.Project.open_library_file(),
which opens a library file through that repair. Route all three
IfcStore.library_file load sites in SelectLibraryFile through it. Downstream
code (get_root_context, ProjectLibraryData, RefreshLibrary,
AddProjectLibrary) now always operates on a spec-valid model, so the
duplicate enum entry cannot occur; the previous one-line dedup guard in
parent_libraries_enum() is kept only as cheap defense in depth for callers
that bypass the load-time repair, not as the fix.

Rework test_project_library_data.py: the previous _make_library_only_file()
fixture built an invalid library-only model and asserted that as correct
behaviour. Replace it with a spec-valid fixture (IfcProject + IfcProjectLibrary
declared to it) for the downstream tests, and a malformed fixture used only to
exercise the new repair path.

Verified live in headless Blender (isolated profile): reproduced the original
duplicate-enum-key failure mode, then confirmed ensure_project_context/
open_library_file repair a malformed file and ProjectLibraryData,
refresh_library and add_project_library all operate correctly on the result,
with no duplicate keys and no regression on already-valid files or IFC2X3.

This change was made with the assistance of an AI tool.

* Bonsai: stop supporting library-only files, do not repair them

Per Moult's feedback: if the IFC is invalid, our default position is to not
support it, not to patch around it. A library file with no IfcProject is
invalid IFC (Project Context concept template requires exactly one
IfcProject), and it is not ubiquitous: every library file bonsai ships under
bim/data/libraries has an IfcProject with the IfcProjectLibrary declared to
it via IfcRelDeclares. The single #8183 report is an outlier, not a common
authoring pattern worth accommodating.

Remove tool.Project.ensure_project_context() and open_library_file() (the
load-time repair added in the previous commit here) and revert
SelectLibraryFile's three load sites to plain ifcopenshell.open. Simplify
get_root_context() back to returning ifc_file.by_type("IfcProject")[0]
directly, no IfcProjectLibrary fallback: a file without IfcProject now raises
IndexError instead of being silently treated as valid. AddProjectLibrary's
nest-under-library branch is now dead code (root_context is always an
IfcProject) and is removed. The one-line enum dedup guard from the original
commit here is also removed: since get_root_context can only return an
IfcProject or raise, an IfcProject id can never collide with a library id, so
the guard has nothing left to guard against.

Rework test_project_library_data.py: drop the invalid _make_library_only_file
fixture and its tests, which asserted an unsupported model as correct
behaviour. Replace with a single spec-valid fixture matching bonsai's own
shipped library files (IfcProject + IfcProjectLibrary declared to it), used
for the ci-bonsai-daily regression test and the refresh/add-library
operators, plus one explicit test that get_root_context raises for a file
without IfcProject, documenting that this input is intentionally
unsupported rather than silently tolerated.

Verified live in headless Blender (isolated profile, source-loaded, never
the real profile): confirmed the removed methods are gone, that a
library-only file now raises instead of being handled, that
ProjectLibraryData/refresh_library/add_project_library all work correctly
on a spec-valid model with unique enum keys, and spot-checked that every
library file under bim/data/libraries already has an IfcProject.

This change was made with the assistance of an AI tool.

* Bonsai: inline get_root_context, trim docstrings, confirm get_parent_library unchanged

Per Moult's round 3 review. get_root_context added nothing over
ifc_file.by_type("IfcProject")[0], which is guaranteed by the IFC Project
Context concept template; remove it and inline the call at its three sites
(operator.py's RefreshLibrary and AddProjectLibrary, data.py's
parent_libraries_enum). Trim the get_parent_library docstring to one line;
its logic is untouched by this PR, byte for byte identical to origin/v0.8.0,
and still returns None only when project_library has neither Nests nor
HasContext, never for a library declared directly to IfcProject.

Rework test_project_library_data.py to match: replace the two
get_root_context-specific tests with one that exercises the real call site
(ProjectLibraryData.parent_libraries_enum raising IndexError for a file
without IfcProject), and add an explicit test that get_parent_library
returns None for a genuinely orphaned library. Also drop a long inline
comment that restated what the test body already shows.

Verified live in headless Blender (isolated profile, source-loaded, never
the real profile): all 17 test/bim/module/project tests pass, including the
new get_parent_library None-for-orphan case. Ran the full test/bim suite
before and after on the identical harness: 82 failed/1335 passed both times,
same failing tests (all pre-existing, unrelated to this module).

This change was made with the assistance of an AI tool.

* Bonsai: fix EditProjectLibrary leaving stale declarations after reparenting

Per Moult's round 4 review. The assertion change (get_parent_library(root)
now returns the IfcProject instead of None) is correct: in the old
library-only test model a top-level library had neither IfcRelNests nor
IfcRelDeclares, so None meant "top level". In the new spec-valid model a
top-level library is always declared to the guaranteed IfcProject via
IfcRelDeclares, so get_parent_library correctly resolves it through the
HasContext branch instead of falling through to None. get_project_hierarchy
already keys top-level libraries under the project for exactly this reason,
so the library tree still renders correctly.

Auditing every caller found one real bug in EditProjectLibrary, which
Gorgious56 originally wrote for the library-only model. Its move-library
logic assumed a top-level library (previous_parent_library is None) needed
no cleanup before nesting it under a new parent, and that unnesting a
library back to the project needed no new relationship because it was
"already assigned by default". Both assumptions relied on a top-level
library never actually holding a IfcRelDeclares, which is no longer true.
Reproduced live: moving a project-declared library under another library
left its old IfcRelDeclares dangling alongside the new IfcRelNests (an
invalid double parentage), and moving a nested library back to the project
left it with neither relationship, orphaning it out of the tree entirely.

Fixed by tearing down whichever of IfcRelDeclares/IfcRelNests the library
previously had before establishing whichever one the new parent requires,
instead of assuming which prior state applies.

Added tests: get_parent_library resolving a nested sub-library to its
library parent (the third contract case alongside project-declared and
orphaned), and both EditProjectLibrary reparenting directions, which fail
without the operator.py fix and pass with it.

Verified live in headless Blender (isolated profile, source-loaded, never
the real profile): all 20 test/bim/module/project tests pass. Ran the full
test/bim suite before and after on the identical harness: 123 failed/1294
passed before, 123 failed/1297 passed after, identical failing test names
in both runs (diffed), the extra 3 passes are the new tests above.

This change was made with the assistance of an AI tool.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit d1f9e5243e)
2026-07-25 23:18:33 +10:00
Petru Conduraru 2a5baa281e Fix ci-bonsai-daily: guard on_depsgraph_update_caps during file load
on_depsgraph_update and on_depsgraph_update_caps are registered together
as persistent depsgraph handlers (bim/module/clip_box/__init__.py:50-52).
on_depsgraph_update guards with `if cls._file_loading: return`, but the
sibling on_depsgraph_update_caps did not, so a depsgraph tick during the
file-load window still ran it. Beyond the failing test, this can re-arm a
cap-rebuild bpy.app.timers callback in the exact load window _on_load_pre
cancels timers for, against regions whose GPU state is not yet wired.

Add the same _file_loading guard as the first check.

Verified in headless Blender:
test_clip_box.py::TestRefreshTimerLifecycle::test_depsgraph_update_no_op_while_loading
1 failed -> passed.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 537317b26f)
2026-07-25 23:18:33 +10:00
Petru Conduraru cf4d797b79 Bonsai: don't crash querying a freshly linked IFC with cache off
Link IFC with 'Use Cache' unchecked crashed with FileNotFoundError
when no .ifc.cache.blend existed yet (a fresh link). Regression from
35e3d9c42, which refactored the cache-clear guard from
'if not self.use_cache and blend_filepath.exists()' into
should_clear_cache() but dropped the existence check on the
not-use_cache path, so os.remove() ran on a non-existent file.

Check blend_filepath.exists() first in should_clear_cache() so the
remove is never attempted when there is nothing to clear, while
keeping the query-mismatch cache invalidation intact.

Fixes #8350

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 4a095f9810)
2026-07-25 23:18:33 +10:00
falken10vdl 2d453492cc Cache last shading type to skip redundant material style restores
(cherry picked from commit c92a825a94)
2026-07-25 23:18:33 +10:00
falken10vdl 8face85629 add material update_tag in restore_material_style_types
(cherry picked from commit 4c5bd88877)
2026-07-25 23:18:33 +10:00
falken10vdl 1e7cf4533a Use consistent material style prop accessor
(cherry picked from commit 92e3e400f8)
2026-07-25 23:18:33 +10:00
falken10vdl 702f65c29c Fix initila style when loading (default is SOLID - Flat: Shade)
(cherry picked from commit 13c4ba257d)
2026-07-25 23:18:33 +10:00
falken10vdl 040b02dd2d Add Flat/Pretty style toggle and dual-branch external style management
(cherry picked from commit c77a28c862)
2026-07-25 23:18:33 +10:00
Petru Conduraru 1416af510a style.assign_representation_styles: fix crash on IfcPresentationStyleAssignment #7883
When replacing a style on an item whose previous IfcStyledItem wraps its styles
in the deprecated IfcPresentationStyleAssignment, and the assignment is not
being reused (use_style_assignment is False, e.g. an IFC4 file authored by
AVEVA E3D), the else branch called remove_same_type_styles(style_assignment)
with style_assignment still None, raising
AttributeError: 'NoneType' object has no attribute 'Styles'. Operate on style_,
the assignment found in the current iteration, instead of the accumulator.
Verified red-green with a minimal IFC4 file using IfcPresentationStyleAssignment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 82f73c29ea)
2026-07-25 23:18:33 +10:00
Petru Conduraru f761156dea Selector: negate list comparisons as an aggregate #8129
compare() recursed into list values passing the negated comparison through,
so != meant "at least one item differs" and both = and != matched the same
elements on any multi-valued property (e.g. an enumerated property with two
values selected). Strip the negation for the per-item comparison and negate
the aggregate instead, so != means "no item equals" and stays the complement
of =. The same applies to !*=.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 209c44db83)
2026-07-25 23:18:33 +10:00
Petru Conduraru ad6f9bbbf6 ifcpatch: use stdlib graphlib for Optimise topological sort (#4399)
The Optimise recipe imported `toposort`, a third-party PyPI package that
is not bundled with Bonsai, so running the recipe there raised
`ModuleNotFoundError: No module named 'toposort'`.

Replace it with the standard library `graphlib.TopologicalSorter`
(available since Python 3.9), which provides the same dependencies-first
ordering guarantee the recipe relies on: forward-referenced instances are
mapped before the instances that reference them. The dependency-graph
dict format ({node: {predecessors}}) is identical between the two, so the
graph construction is unchanged. Drop `toposort` from ifcpatch's
dependencies since it is no longer used.

Verified with toposort NOT installed: the Optimise recipe now runs and
deduplicates correctly (IfcParseExamples_test.ifc 88 -> 63 instances, all
6 products preserved, output reopens cleanly).

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 382f5e0c21)
2026-07-25 23:18:33 +10:00
Petru Conduraru 13c65eb00d Bonsai tests: give bSDDClientStub the client baseurl attribute
tool.Bsdd.identifier_url() (pset/ui.py pset name check in the Property
Sets panel) reads client.baseurl unconditionally, but the test stub
never had that attribute, so any scenario that opens the Property Sets
panel dies with AttributeError under the stub. The boolean.feature
scenarios only surfaced this once their STEP id failures were fixed,
the id failure had been masking it. Mirror the real bsdd.Client
default so identifier_url() resolves to the standard identifier URL.

This change was made with the assistance of an AI tool.

(cherry picked from commit 81a42cec1a)
2026-07-25 23:18:33 +10:00
Petru Conduraru d5f9c7b5c3 Bonsai tests: stop hardcoding STEP ids in boolean.feature
The two boolean.feature scenarios pinned representation item objects by
absolute STEP id (Item/IfcHalfSpaceSolid/90, the BBIM_Boolean pset text
[91]). Those ids shift every time any earlier entity allocation in an
empty project changes (latest instance: #8577 moved 90 to 86), so this
cluster re-breaks on unrelated commits.

Make the object-name and panel-text BDD steps run their argument through
replace_variables, the same substitution 'the variable' and the
connection steps already use, and have boolean.feature capture the real
ids from the IFC file (by_type(...)[0].id()) into variables at the point
the entities are created. The steps stay strict: the substituted name
must still resolve to exactly the named object, there is no wildcard
matching. Substitution is a no-op for every existing feature string
without a {variable} placeholder.

This change was made with the assistance of an AI tool.

(cherry picked from commit 45fa04a94b)
2026-07-25 23:18:33 +10:00
Petru Conduraru 4cd7a3a1ef Fix ci-bonsai-daily BDD: OperatorSpy.bl_rna + stale MEP port name
Two independent test-harness/fixture defects in test/bim/test_feature.py:

- OperatorSpy had no bl_rna, so any BDD step that redraws a panel calling
  helper.draw_filter() (which tests "module" in op.bl_rna.properties)
  crashed with AttributeError. Give OperatorSpy a bl_rna property that
  forwards to the real registered operator class
  (bpy.types[bl_idname].bl_rna), matching live UILayout.operator()
  semantics. Fixes test_select_all_walls and test_edit_filter_query.
- The shared "I create default MEP types" step looked up
  bpy.data.objects["IfcDistributionPort/Port"], but port creation never
  sets port.Name, so tool.Loader.get_name deterministically names the
  object "IfcDistributionPort/Unnamed". Update the literal. Fixes the MEP
  scenarios (connect/transition/bend) that share this setup.

Verified in headless Blender: OperatorSpy scenarios 2 passed (were
AttributeError); MEP test_connect_mep_elements* go from
KeyError 'IfcDistributionPort/Port' to passing.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit e27624c77f)
2026-07-25 23:18:33 +10:00
Ryan Schultz 9c8c159467 Fix #7774: Fix Select Similar failing on pset names with spaces
Pset names containing spaces (e.g. "SOLIDWORKS Custom Properties") were
not quoted when building selector keys in SelectSimilarData, causing
get_element_value to fail when the operator ran. Now wraps pset names
and property names in double quotes if they contain spaces, consistent
with the selector syntax used elsewhere.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 836d57e7ff)
2026-07-25 23:18:33 +10:00
Petru Conduraru a44e27e62e ci: drop the ColumnPSetsOfSets.ifc fixture change, conflicts upstream
Per aothms's review comment: this file's schema was already changed
independently on v0.8.0 since this branch was created, so this PR's own
edit conflicts with it. Reverting to the current upstream version of the
fixture; the bsdd.py rate-limiting fix is untouched.

(cherry picked from commit 3d7d1ff4f3)
2026-07-25 23:18:33 +10:00
Petru Conduraru 38a005ccf5 ci: fix bSDD 429 rate limiting and restore ColumnPSetsOfSets.ifc schema
bsdd.py: the Client made every request with a bare requests.get, so a single
429 from the (unauthenticated, aggressively rate limited) bSDD API failed the
whole test. Route requests through a Session with a mounted urllib3 Retry
(5 attempts, backoff, honouring Retry-After) for 429/5xx, matching how a
resilient API client should behave, not just papering over the test.

ColumnPSetsOfSets.ifc: FILE_SCHEMA was accidentally changed from IFC4X3_ADD2
to IFC2X3 in a7738eeb64 (an unrelated logger refactor), a one line collateral
edit to this fixture. The file's DATA section still uses IFCPROPERTYSETDEFINITIONSET,
an IFC4+ only type. Parsing it against IFC2X3 threw "Entity ... not found in
schema", which silently fell back to interpreting the value as a raw nested
aggregate instead of the intended defined-type wrapper, producing the
double-nested tuple that broke test_stream, test_file and test_rocks in
test_streaming_rocksdb_and_simpletyperefs.py. Restoring the original schema
declared when the fixture was added (ff3fa48332) fixes all three.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 6911418c67)
2026-07-25 23:18:33 +10:00
Petru Conduraru 19c048fca3 Fix ci-bonsai-daily: get_dictionaries no longer clobbers injected client
Bsdd.get_dictionaries() unconditionally did cls.client = bsdd.Client(),
replacing whatever client was already set - including the
bSDDClientStub the BDD suite injects at module load
(test_feature.py: tool.Bsdd.client = bSDDClientStub()) to avoid live
network calls. Because "Load bSDD Dictionaries" is the first step of
every bsdd.feature scenario, the stub was discarded before its fixture
data ("LCA", "BonsaiTestDict") could ever be returned.

The re-init is unnecessary: bsdd.Client.__init__ only sets baseurl and
blank tokens, and the next line already updates baseurl defensively via
hasattr. Drop the clobbering assignment; reuse whichever client is
already set.

Verified in headless Blender: bsdd scenarios (load dictionaries, search
all/single dictionary) go from 3 failed ("Could not see LCA/
BonsaiTestDict") to 3 passed.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 5c8eab981c)
2026-07-25 23:18:32 +10:00
Petru Conduraru 3ed016b3d0 Fix ci-bonsai-choco: choco_release.py still targets the old choco/blenderbim path
The choco dir was renamed from choco/blenderbim to choco/bonsai back in
2024 (Rename choco dir), but choco_release.py's BLENDERBIM_DIR constant
was never updated, so the daily choco release job crashes immediately
with FileNotFoundError trying to os.chdir into the now nonexistent
choco/blenderbim directory.

Release tags also moved from a bare blenderbim-YYMMDD scheme to
bonsai-X.Y.Z-alphaYYMMDDHHMM, so the tag-prefix strip used to build the
package version still looked for the old "blenderbim-" prefix and left
it untouched, embedding the raw tag (including the already-present
"-alpha" segment) into the nuspec version field, which the template
then doubled up with its own "-alpha" suffix, producing an invalid
NuGet version string. Both are fixed together since the second bug
would otherwise surface as soon as the first one is unblocked.

The pre-commit black hook also reformatted pre-existing whitespace
drift in choco_release.py (this file sits outside CI's lint scope, so
it had never been auto-formatted before); that reformatting is
incidental to satisfying the local hook, not part of the fix itself.

Generated with the assistance of an AI coding tool.

(cherry picked from commit a155a1ca80)
2026-07-25 23:18:32 +10:00
Petru Conduraru 5541bad23f ifcopenshell.util.element: dedupe SET-typed attributes in replace_attribute
replace_attribute() rewrites references inside aggregate attributes via
element.walk(), but never checked whether the replacement value was
already present elsewhere in the same aggregate. For an EXPRESS SET
(e.g. IfcProject.RepresentationContexts, IfcRelAggregates.RelatedObjects)
this can leave the same reference listed twice, which is invalid IFC.
LIST and BAG aggregates legitimately allow duplicates, so a blanket dedup
would be wrong; only SET-typed attributes are deduplicated, determined at
runtime from the schema declaration (IfcOpenShell#8706 review comment).

The SET/LIST/BAG check is cached per (schema, class, attribute index), and
the dedup pass itself only runs when a cheap linear pre-check finds the
replacement value already present in the aggregate, so the common case
(no duplicate produced) pays only that pre-check, not a hash-set rebuild.
Benchmarked against a 23MB (431k entities) and a 104MB (2.4M entities) IFC
model against a large SET attribute: worst case adds well under 1ms per
call; the realistic case (merging duplicate contexts, matching the PR
#8706 scenario) shows no measurable regression.

Fixes the root cause flagged in IfcOpenShell#8706 (Moult), obviating the
need for MergeDuplicateContexts' own manual aggregate-dedup pass for that
scenario.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 8c434b7167)
2026-07-25 23:18:32 +10:00
Petru Conduraru 7cf172b96b docs: cover reading properties and quantities from an element and its type in C++ getting started
Fixes issue #3910's documentation gap. IsDefinedBy() returns
IfcRelDefinesByProperties relationship objects, not the property set
itself, and RelatingPropertyDefinition() must be used to reach the
IfcPropertySet or IfcElementQuantity. Properties can also come from an
element's type via IsTypedBy() -> RelatingType() -> HasPropertySets(),
a path that is easy to miss because it works differently. Adds a
worked, beginner-commented, compilable example covering both paths.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 8fb8966094)
2026-07-25 23:18:32 +10:00
Petru Conduraru 33f1aa8104 Bonsai: compute earthworks base quantities in ifc5d take-off #6325
The ifcopenshell take-off engine left every Qto_EarthworksFillBaseQuantities
value null, so Bonsai added the qset with no numbers on IFC4X3 models. Map the
geometrically derivable quantities using the slab axis convention: Length on
local X, Width on local Y, Depth on local Z, and the net solid volume as the
compacted (Fill) or undisturbed (Cut) volume. LooseVolume and Weight stay
unmapped because they need soil bulking and density factors absent from
geometry. Bring IfcEarthworksCut to parity with the Blender engine and wire
IfcReinforcedSoil on both engines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit d506f5df1e)
2026-07-25 23:18:32 +10:00
Petru Conduraru 0bac7f5542 Fix #7331: derive cost item quantities from IfcSpace base quantities
assign_cost_item_quantity skipped every IfcSpatialElement, which also
swallowed IfcSpace. Spaces are legitimate quantifiable objects, so their
Qto_SpaceBaseQuantities (for example GrossFloorArea) were never picked up
and count based cost items fell back to 0. Keep skipping spatial
containers (site, building, storey) but allow IfcSpace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit e23142d01a)
2026-07-25 23:18:32 +10:00
Petru Conduraru 3f42e02cb4 Fix #8570: populate ifc5d IfcOpenShell QTO formulas for IfcSpace
The headless "IfcOpenShell" calculator had all Qto_SpaceBaseQuantities
formulas set to null for IfcSpace in both IFC4QtoBaseQuantities.json and
IFC4X3QtoBaseQuantities.json, so qto.py's `if not formula: continue`
skipped every quantity, no geometry task was queued, and spaces never
appeared in results (elements_quantified: 0). The Blender calculator
already computes these; they were just never ported to the
ifcopenshell.util.shape-backed calculator.

Map the eight computable quantities to existing util.shape functions,
mirroring the Blender calculator semantics (no new shape.py code):
GrossFloorArea=gross_get_footprint_area, NetFloorArea=net_get_footprint_area,
GrossCeilingArea=gross_get_top_area, NetCeilingArea=net_get_top_area,
GrossPerimeter=gross_get_footprint_perimeter, GrossVolume=gross_get_volume,
NetVolume=net_get_volume, Height=net_get_z.

Left null (matching the Blender ruleset, not guessed): GrossWallArea,
NetWallArea, NetPerimeter (Blender stub), and FinishFloor/CeilingHeight
(Blender derives these from sibling IfcCovering decomposition geometry,
which this per-element calculator architecture can't reach).

Verified on IFC4 (4x3 space extruded 2.5m): before -> {} / elements_quantified 0;
after -> GrossFloorArea 12, GrossPerimeter 14, Height 2.5, GrossVolume 30,
etc. - all exact matches to the extrusion. IFC4X3 formulas are identical
and the formula->function resolution is schema-agnostic.

Scope: fixes the IfcSpace case (the issue title). The 12 other all-null
classes noted in the issue (IfcDoor, IfcSite, IfcRailing, ...) are left as
follow-up.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 6085894433)
2026-07-25 23:18:32 +10:00
Petru Conduraru 87dd026f94 Fix IfcCovering Qto_CoveringBaseQuantities mismatch between calculators (#6728)
The ifc5d IfcOpenShell (geometry-based) Qto engine computed
Qto_CoveringBaseQuantities using axis-agnostic heuristics:

- GrossArea/NetArea: gross_get_max_side_area / net_get_max_side_area,
  the largest of the X/Y/Z projected side areas.
- Width: gross_get_min_xyz, the smallest of the X/Y/Z dimensions.

The Blender Qto engine instead already used
EPset_Parametric.LayerSetDirection (AXIS2 for wall-like coverings,
AXIS3 for floor/ceiling-like coverings) to pick the correct axis via
get_covering_gross_area/get_covering_net_area/get_covering_width in
bonsai/bim/module/qto/calculator.py.

For any covering whose length isn't the largest dimension (e.g. a
short wall-covering strip, or a small covering patch), the two
engines' heuristics can pick different faces/axes entirely, giving
different Width/Area values for the same element - this is what was
reported in #6728.

Fix: give the IfcOpenShell engine the same layer-set-direction
awareness. Added IfcOpenShell.get_covering_parametric_axis/
get_covering_area/get_covering_width (dispatched as internal
functions, like the existing get_weight/get_segment_length), and
wired gross_get_covering_area/net_get_covering_area/
gross_get_covering_width into the IfcCovering rules in
IFC4QtoBaseQuantities.json and IFC4X3QtoBaseQuantities.json.

The AXIS2 area/width formulas (get_side_area, net_get_y) intentionally
match the simpler formulas already used for Qto_WallBaseQuantities in
this same rule set (net_get_side_area/net_get_y), rather than
replicating the Blender engine's more elaborate get_lateral_area/
get_width (min(X,Y)) helpers, consistent with how the two engines
already diverge for regular walls without being considered a bug.

Verified with a standalone script driving ifc5d.qto.IfcOpenShell
directly against synthetic AXIS2/AXIS3 IfcCovering geometry: for
typical proportions old and new formulas agree, and for
disproportionate coverings (thin dimension not the smallest/largest)
the old formulas picked the wrong axis while the new ones correctly
track the covering's LayerSetDirection, matching the Blender engine.
Did not verify through the full Blender/Bonsai UI, as it would have
required registering the addon in the machine's shared Blender
profile, which is unsafe while other agents may have it loaded.

Generated with the assistance of an AI coding tool.

(cherry picked from commit c61ebe3876)
2026-07-25 23:18:32 +10:00
carlopav 2292832a17 cost: don't leave copied cost items in the copied schedule (#8851)
copy_cost_item appends the copy to the inverse relationships of the
original cost item, which for a root cost item includes the source
schedule's IfcRelAssignsToControl. copy_cost_schedule then assigned that
same cost item to the new schedule as well, so the copies showed up in
both schedules and deleting them from one removed them from the other.

Unassign the copy from the source schedule before assigning it to the
new one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 305f8c6003)
2026-07-25 23:18:32 +10:00
Petru Conduraru fd05d5f360 Bonsai: wire Shift+Q quantity take off hotkey into Spatial tool
Fixes #4443. The Wall/Slab/other authoring tools (BimTool subclasses)
already bind Shift+Q to bim.perform_quantity_take_off via hotkey_S_Q,
but the Spatial tool has its own separate keymap/operator
(bim.spatial_hotkey) that never registered a Q entry, forcing users to
switch tools just to (re)calculate quantities for a selected element.
Added the same Shift+Q keymap entry and a matching hotkey_S_Q handler
to the Spatial tool, mirroring BimTool's existing behavior exactly
(including the same selected-objects guard).

The other part of the request, a bulk "calculate all quantities"
entry point, already exists today: bim.perform_quantity_take_off
computes quantities for every IfcElement when no objects are
selected, exposed via the Scene > Quantity Take-off panel regardless
of which workspace tool is active, so no change was needed there.

AI-generated, reviewed and tested by BIMvoice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 98a25eca96)
2026-07-25 23:18:32 +10:00
Petru Conduraru 5c0a5cb00f Bonsai: allow nesting element type objects together (#2283)
can_nest() only permitted IfcElement-to-IfcElement pairs, so nesting
two IfcElementType objects (e.g. an IfcElementAssemblyType nesting a
component IfcDoorType) was silently rejected. IfcRelNests.RelatingObject/
RelatedObjects are typed as the general IfcObjectDefinition in the
schema, so type-to-type nesting is schema legal, IfcOpenShell's core
nest.assign_object API already handles it generically, and the Nest
UI panel is driven purely by ifcopenshell.util.element.get_nest/
get_components (IFC data queries, not Blender collection structure),
so once the relationship exists it displays correctly with no other
changes needed.

Extended is_compatible_class to also accept a same-kind IfcTypeProduct
pair. Mixing an occurrence element with a type is intentionally still
rejected, that isn't a real modeling pattern.

Verified live in headless Blender: type-to-type nesting now creates
a real IfcRelNests and the Nest panel's own data functions reflect
it correctly; mixing an occurrence with a type is still rejected;
existing element-to-element nesting is unaffected.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 99c370b755)
2026-07-25 23:18:32 +10:00
Petru Conduraru c5f25405a2 ifc5d: wire up GrossFootprintArea/NetFootprintArea for IfcWall QTO
Root cause: the IfcOpenShell-geometry-engine calculator ruleset
(IFC4QtoBaseQuantities.json and IFC4X3QtoBaseQuantities.json) left
IfcWall's GrossFootprintArea/NetFootprintArea mapped to null, so
these two quantities were silently omitted from Qto_WallBaseQuantities
whenever that ruleset was used. The generic gross_get_footprint_area
and net_get_footprint_area formulas already exist and are already
wired up for IfcSlab in the same files, so this was a missing mapping,
not a missing implementation.

Fixes #7029.

Generated with the assistance of an AI coding tool.

(cherry picked from commit f3cb7aea61)
2026-07-25 23:18:32 +10:00
Petru Conduraru 0d8813348c Fix IfcFooting Qto_FootingBaseQuantities axis mapping per predefined type #4783
Footings are authored two ways with different local axis conventions. Beam-like
footings (STRIP_FOOTING, FOOTING_BEAM) are a profile extruded along local Z, so
Length is local Z and the cross section sits on local X (Width, horizontal) and
local Y (Height, vertical). Slab-like footings (PAD_FOOTING, PILE_CAP) have their
footprint on local X/Y and their thickness (Height) on local Z.

The engine rule set is keyed per IfcFooting and cannot branch on predefined type,
so the previous static rule (Height=net_get_z, Length=net_get_max_xy, Width=null)
swapped Length and Height for beam-like footings and never emitted Width.

Add predefined-type-aware get_footing_length/width/height to the IfcOpenShell and
Blender calculators, and point the IfcFooting rule at them in all four IFC4/IFC4X3
ios/Blender rule files.

Confirmed by authoring footings through the real Bonsai generators and measuring
world-axis orientation: a beam-like footing with a 0.3 wide by 0.6 tall cross
section and 6.0 run reports Length 6.0, Width 0.3, Height 0.6, with the 0.3
physically horizontal and 0.6 physically vertical; a 2.0x1.5x0.3 pad reports
Length 2.0, Width 1.5, Height 0.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 4ceadd8f10)
2026-07-25 23:18:32 +10:00
Petru Conduraru be56984e12 ifc5d: measure openings in their real orientation on both take-off engines
See #6835. Qto_OpeningElementBaseQuantities came out axis-scrambled for
openings authored in a Z-up local frame (X along the voided wall, Y
through it, Z vertical), which is how Bonsai authors every wall opening:

- The IfcOpenShell engine mapped Height to the local Y extent and Depth
  to the local Z extent, so a 0.9 x 2.0 door opening with Bonsai's
  default 1.2m void depth reported Height 1.2 and Depth 2.0, and Area
  (max side area) picked the through-wall side, 2.4 instead of 1.8.
  This matches the wrong Height=1.2/Area=1.2 screenshots reported for a
  1x1 window opening in #6835.
- The Blender engine mapped opening Width to get_length, which returns
  the longest bounding box edge, i.e. the opening height for typical
  door openings (the same defect 4adaf0d fixed for IfcDoor Width), and
  get_opening_depth used min(x, y), which returns the opening width
  whenever the width is smaller than the void depth.

The IfcOpenShell engine now has opening-aware internal calculators
(get_opening_width/height/depth/area) that detect horizontal (slab
style) openings with the same heuristic as the Blender calculator, so
slab opening depths keep reporting the slab thickness. The Blender
ruleset uses get_x for opening Width, and get_opening_depth measures the
through-element Y extent for vertical openings.

Door and window quantities themselves are addressed separately: the
Blender engine door Width was fixed in 4adaf0d, and the remaining
door/window defects (door not quantified on the IfcOpenShell engine,
inflated areas) are fixed by the attribute-based calculators in #8389.

Generated with the assistance of an AI coding tool.

(cherry picked from commit efac8a0ec0)
2026-07-25 23:18:32 +10:00
Petru Conduraru 297d8c981f Bonsai: refresh material data unconditionally instead of forcing a redraw
falken10vdl reviewed 16b1b4e7b1 on #8843 and pointed out that tagging
every area for redraw was overkill. The actual problem was that the
Object Material panel and the scene Materials list read from plain
python caches (ObjectMaterialData and MaterialsData) that only get
invalidated when the Materials editing UI list is reloaded, which
never happens while you are not in editing mode. The redraw itself was
never the issue, closing the rename dialog already triggers one.

Removed the tag_redraw loop from RenameMaterial and instead call the
existing bonsai.bim.module.material.data.refresh() function from
core.rename_material, unconditionally, through a new tool.Material.refresh()
method. This is the same invalidate-on-next-load mechanism already used
by every other module's Data classes, just wired up for this operator
too, instead of introducing a new one.

Also updates the core tests to prescribe the new unconditional refresh()
call, and adds tool-layer coverage for tool.Material.refresh().

Generated with the assistance of an AI coding tool.

(cherry picked from commit 7ab0628c54)
2026-07-25 23:18:32 +10:00
Petru Conduraru 70f6b886b3 Bonsai: refresh the UI after renaming a material
theoryshaw tested #8843 and asked for the new name to show up right
away instead of needing a manual refresh. The Object Material panel
and the scene Materials list both already re-read live IFC data on
their next draw (tool.Ifc.Operator purges those caches after every
IFC-mutating operator), so the button text was correct on the next
redraw. What was missing was the redraw itself: the material name is
a plain button label, not an RNA property Blender tracks, so nothing
told the Properties editor to repaint after the rename dialog closed.
Tag every area for redraw once the rename completes, the same pattern
used elsewhere in Bonsai for popup-triggered edits that need an
immediate repaint.

Also adds core-layer test coverage for rename_material, which had
none.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 16b1b4e7b1)
2026-07-25 23:18:32 +10:00
Petru Conduraru d1c4b39e17 Bonsai: right-click rename on a material name (#6680)
Adds a "Rename Material" entry to the context menu that already
extends every button in the properties editor (UI_MT_button_context_menu),
triggered when right-clicking a material name button
(bim.select_by_material) that points to a real IfcMaterial. This
gives a quick entry point to renaming from the Object Material panel
without navigating to the scene Materials list.

This follows the pattern that #6680's thread converged on: theoryshaw
requested a right-click entry (rather than a pencil icon or
double-click) that keeps the existing single-click select-by-material
behaviour intact. falken10vdl is the issue's assignee; this is offered
as a starting point for that discussion, not a replacement for it.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 8c667b8ae0)
2026-07-25 23:18:32 +10:00
Ryan Schultz 3885d98def Bonsai: add category-level select-all to the Drawings list (#8826)
Add an "Is Selected" checkbox to each target-view category header in
BIM_UL_drawinglist that toggles selection for all drawings in the
category. The toggle only affects drawings currently visible in the
list (honoring the show_drawings_on_sheets_only filter), and the header
checkbox reflects the aggregate selection state of its drawings.

Also make category headers more obvious: wrap them in a box() for a
distinct inset background and make the header name clickable to
expand/contract the category (same as the disclosure triangle).

Ref: #8825

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit e52e5e2e58)
2026-07-25 23:18:32 +10:00
Ryan Schultz 46a0c4b00a Bonsai: add toggle to show only drawings placed on sheets (#8824)
Adds a "Show Only Drawings on Sheets" toggle below the drawing list. When
enabled, the list is filtered to drawings referenced by at least one sheet
(target-view headers with no sheeted drawings are hidden too), and
bim.select_all_drawings only acts on the visible/filtered drawings.

A drawing is considered sheeted when its drawing document Location matches a
document reference Location on any SHEET-scoped IfcDocumentInformation.
Filtering is computed live so it reflects sheet edits without reloading.

Closes #8823

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 2d59ea1988)
2026-07-25 23:18:32 +10:00
Petru Conduraru 8c5dbc3671 docs: cover the Blender 5.1 / Python 3.13 transition in installation guides (#8781)
* docs: cover the Blender 5.1 / Python 3.13 transition in installation guides

The system requirements still listed Blender 4.3-4.5 with Python 3.11
only, and nothing documented the pitfall from issue 7623: importing
preferences into a Blender whose Python version changed carries over an
incompatible Bonsai build that silently fails to load. Document the two
Python generations, that Get Extensions picks the matching build
automatically while manual zip installs do not, and the
uninstall-reinstall step that resolves the upgrade case.

Generated with the assistance of an AI coding tool.

* docs: keep it simple, only Blender 5.1 and 5.2 with Python 3.13

Per review, drop the descriptive text and the Python 3.11 line.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 55a2430d71)
2026-07-25 23:18:32 +10:00
Petru Conduraru cb15b6a49f Preserve the real cause when the ifcopenshell wrapper fails to load (#8785)
* Keep real cause in wrapper ImportError

When the compiled wrapper exists for the current interpreter but fails
to load (for example a glibc version mismatch, as on AWS Lambda in
issue 5927), the bare except rewrote the error into the misleading
"IfcOpenShell not built for '<platform>'" message. Environments such
as AWS Lambda or the Blender add-on dialog only surface the final
exception message, so the actual cause was invisible and undiagnosable.

Keep the "not built for" message only when no matching binary is
present, and otherwise include the original loader error, chaining the
cause in both branches.

This change was AI-generated.

Fixes #5927

* Simplify wrapper import failure to a single message

Per review feedback, drop the filesystem scan and the two message
variants. Always raise the classic "IfcOpenShell not built for
'<platform>'" message with the original exception appended in
parentheses, still chained as the cause. Environments that only show
the final exception message (AWS Lambda, the Blender add-on dialog)
now surface the real loader error, such as the glibc version mismatch
in issue 5927, without any extra logic.

This change was AI-generated.

(cherry picked from commit 04a2535a98)
2026-07-25 23:18:32 +10:00
Petru Conduraru 43fb8c5baa Bonsai: add Hour zoom level to the interactive Gantt chart
The jsGantt-improved library that renders Bonsai's Gantt chart already
ships full support for an "Hour" granularity (column width, header
labels in every bundled language, hour-aware rendering math). Bonsai's
config only exposed Day/Week/Month/Quarter, with a comment claiming
Hour caused browser issues even with vUseSingleCell enabled.

Headless Chrome testing against the same library version shows that
claim no longer holds once vUseSingleCell is active (as Bonsai already
configures it at 10000): Hour-format charts render without errors from
typical schedules up through fairly extreme ones (5000 tasks across a
3 year span rendered in about 2.4s). The failure mode the old comment
described only reproduces with vUseSingleCell disabled, which is not
how Bonsai runs it.

Task start/finish times already flow through to the chart unmodified
as raw ISO datetimes (tool/sequence.py create_new_task_json), so any
schedule authored with real hour-level timestamps, for example an
imported MS Project/P6/Excel schedule or one written directly through
ifcopenshell-python, can now be viewed at hour granularity. Verified
live with a night shift schedule crossing midnight, rendered correctly
with no console errors.

Note: Bonsai's own "Edit Task Time" UI currently always snaps
ScheduleStart/ScheduleFinish to 09:00/17:00 regardless of the hour
entered (ifcopenshell/api/sequence/edit_task_time.py), and work
calendars only encode working days, not working hours. So authoring a
genuine hour-precision schedule through that UI is still not possible;
this change only unlocks viewing hour-level data that already exists
in the model. Fixing the editor and calendar model is a separate,
larger design decision for a maintainer.

Addresses #2772.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 727b5f3475)
2026-07-25 23:18:32 +10:00
Bartok 54dfbefc79 docs(ifc2ca): fix script paths in README
Point scriptSalome.py at templates/salome/ and the bonded scripts at
_deprecated/, matching the current tree so README links resolve.

Generated with the assistance of an AI coding tool.

(cherry picked from commit a45f2fae61)
2026-07-25 23:18:32 +10:00
dependabot[bot] 20d7ba53d5 build(deps): bump ruff from 0.15.12 to 0.15.22
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.12 to 0.15.22.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.12...0.15.22)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 7a7a250942)
2026-07-25 23:18:32 +10:00
Petru Conduraru 75d1c29ff6 Bonsai: add one click copy of annotations to another drawing (#8719)
* Bonsai: move annotations between drawings when reassigning their group

Assigning an IfcAnnotation to a group that represents another drawing
previously left the annotation in both drawings at once: it stayed in
its old drawing group, its Blender object stayed in the old drawing
collection, and it kept the old camera depth, so the reassignment
appeared to do nothing useful. Issue #2966 documents the seven step
manual workaround users needed instead.

The assign group operator now detects when the target group represents
a drawing (via the new tool.Drawing.get_group_drawing, the inverse of
get_drawing_group), unassigns the annotation from its previous drawing
group, moves its object into the new drawing collection, and places it
on the new drawing camera plane. The target camera is imported on
demand when it has not been loaded yet, matching the pattern used by
the activate drawing operator.

Generated with the assistance of an AI coding tool.

* Bonsai: add one click copy of annotations to another drawing (#2966)

Duplicating an annotation into a different drawing used to require a
seven step manual process: loading groups in scene properties, copying
the object, fixing its group assignment by hand, and repositioning it
onto the target camera plane. A plain Blender duplicate is not enough
because the copy keeps pointing at the same IFC entity, and the Shift D
override, while it does create a genuine new entity through
root.copy_class, leaves the duplicate in the source drawing group,
collection, and camera depth.

The new copy annotation to drawing operator packages the proven recipe
already used by duplicate drawing into one action: duplicate through
tool.Geometry.duplicate_ifc_objects, unassign the copy from the source
drawing group, assign it to the chosen target group, place it on the
target camera plane at the same world XY, and file it into the target
drawing collection. The originals are left untouched and the user's
selection is restored. The target camera is imported on demand when it
has not been loaded yet.

The operator shows a target drawing dropdown and is reachable from the
annotation tool sidebar when an annotation is selected, and from the
drawings panel. Annotations already in the target drawing are skipped
and reported.

The orchestration lives in core.drawing.copy_annotations_to_drawing
with prophecy tests covering the copy, the skip, and the camera import
branches. Verified live in headless Blender 5.1: the copy is a new
IfcAnnotation with its own GlobalId and IfcTextLiteral, both texts are
editable independently, and everything survives save and reload with
each annotation loading in its own drawing.

Generated with the assistance of an AI coding tool.

(cherry picked from commit bc1fb2a88d)
2026-07-25 23:18:32 +10:00
Petru Conduraru 540e4c9cee bonsai: allow overriding which classes join in section linework (#4395) (#8617)
Fixes #4395.

Root cause: the SVG cut-linework merge step that fuses adjacent
elements' cut polygons together (per the pset-driven JoinCriteria
setting) was hardcoded to only IfcWall and IfcSlab. IfcCovering cut
shapes were skipped unconditionally, so adjacent coverings never
joined, leaving a visible seam/broken corner in section drawings
regardless of JoinCriteria.

Fix: added an EPset_Drawing.JoinClasses property, following the
exact same user-overridable pattern already used by
EPset_Drawing.BringToFront - a comma-separated list of IFC classes
to join, defaulting to "IfcWall,IfcSlab" (unchanged behavior) when
unset. Users can override per-drawing to add IfcCovering (or any
other class) when they want it joined too. Kept this opt-in rather
than hardcoding IfcCovering into the default list, since joining a
thin finish layer the same way as a thick wall/slab could produce
unwanted mitring in some cases - the user decides per drawing.

Verified live against the reporter's own attached file
(ifcovering joining.ifc) and its cached section linework: with
JoinClasses unset, two separate closed paths reproduce the reported
seam exactly. With JoinClasses = "IfcWall,IfcSlab,IfcCovering", the
two coverings merge into a single closed polygon with the internal
seam removed. Confirmed IfcSlab join behavior is unchanged in both
runs.

Generated with the assistance of an AI coding tool.

Co-authored-by: Dion Moult <dionmoult@gmail.com>
(cherry picked from commit c55a79b8b5)
2026-07-25 23:18:32 +10:00
sboddy d9b7450f37 Propagate deflection settings on reload (#8484)
reimport_element_representations() built a fresh
ifcopenshell.geom.settings() without copying deflection_tolerance /
angular_tolerance from the IfcImportSettings it had just
constructed, and never passed geometry_library to either the
iterator() or create_shape() calls it makes. As a result, exiting
Item/edit mode (which reaches this function via
switch_representation) silently fell back to IfcOpenShell's
hard-coded mesher defaults (0.001 linear deflection, ~50x finer than
the project's default of 0.05) and the default geometry kernel,
instead of the project's configured tolerance and Geometry Library.

This made geometry visibly change quality after a no-op Tab into and
back out of edit mode, since the reload path was unintentionally far
more precise (and used a different kernel) than the initial import.
Both settings, and geometry_library, are now taken from the
IfcImportSettings instance already built at the top of the function,
so a reload matches the original import.

Refs #5685.

Generated with the assistance of an AI coding tool.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit b669baf793)
2026-07-25 23:18:32 +10:00
Petru Conduraru c9c78f6127 Bonsai: refresh the arc/circle decorator immediately after duplicating a loop
theoryshaw's follow-up on #6944: after the profile/curve reconstruction fix
(previous commit), the arc/circle marker for a freshly Shift+D-duplicated
loop wouldn't appear until leaving and re-entering Edit Mode.

Root cause: ProfileDecorator groups arc/circle vertices purely by
IFCARCINDEX/IFCCIRCLE vertex-group index every draw call (it has no cache
to go stale, it fully recomputes from the live edit-mesh bmesh each frame).
Duplicating a loop copies its vertex-group weights onto the new geometry,
since Blender allocates no new group for a duplicate, so the source loop
and its live duplicate land in the same dict entry. That entry then fails
the "exactly 2 verts per circle / 3 per arc" check and is skipped entirely,
so BOTH the original and the duplicate stop being drawn until the mesh is
reimported and gets fresh, distinct groups.

Verified live in headless Blender: built a bmesh with an IFCCIRCLE loop and
an IFCARCINDEX loop, then ran bmesh.ops.duplicate on each (the same
bmesh-level operation underlying Shift+D) and called ProfileDecorator's
draw method directly. Before this change, duplicating either loop dropped
both the original and the duplicate from the decorator (0 circle/arc
batches drawn instead of 2). After, both draw immediately, with no change
to the non-duplicated case (still 1) or to genuinely distinct loops (5
independent circles still resolve to 5, not merged). 500-circle timing is
unchanged (~14.3ms/draw before and after), so the added connectivity split
is not a hot-path regression.

Added test/bim/module/model/test_profile_decorator_duplicate_loop.py
pinning the new _connected_components helper's behavior for single and
duplicated circle/arc loops.

This contribution was produced with the assistance of an AI coding tool.

(cherry picked from commit 32ac20e8e3)
2026-07-25 23:18:32 +10:00
Petru Conduraru 946b6dd6b1 Bonsai: fix the same duplicate-loop vertex-group bug in auto_detect_curves
auto_detect_profiles had the identical defect fixed in the previous
commit: duplicating a circle/arc loop in Edit Mode reuses the same
IFCCIRCLE/IFCARCINDEX vertex group index for the new geometry, and this
sibling function (used for curve/annotation editing rather than profile
voids) tallied group membership across the whole mesh instead of per
loop, so it also rejected a legitimately duplicated loop as malformed.

Applied the identical fix: scope the group-count sanity check to each
connected edge loop, computed after the loops are built rather than in
the initial whole-mesh vertex pass. Kept the existing forked-loop check
(more than 2 edges per vertex) in the first pass since it is unrelated
to group counting.

Verified live in headless Blender: constructed two 2-vertex IFCCIRCLE
loops sharing one vertex group index (the exact state Blender's Edit
Mode duplicate produces) and called auto_detect_curves directly.
Before this change it returned (False, "CIRCLE"); after, it returns two
valid IfcCircle curves.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 0d5ea02169)
2026-07-25 23:18:32 +10:00
Petru Conduraru f0f4e0d4ca Bonsai: fix profile reconstruction after duplicating a circle/arc in Edit Mode
Duplicating a circular or filleted-arc void in the profile CAD editor
(Shift+D on the loop's vertices) reused the same IFCCIRCLE/IFCARCINDEX
vertex group index for the new geometry, since Blender's mesh duplicate
copies vertex group weights but does not allocate a new group. On exit
from Edit Mode, auto_detect_profiles tallied group membership across the
whole mesh rather than per loop, so a group meant to hold exactly 2 (circle)
or 3 (arc) vertices ended up with double that, failing its sanity check
and blocking the edit with an "INVALID PROFILE" popup. Fixes #6944.

Scope the sanity check to each connected edge loop instead, matching how
the loops are actually converted into IfcCircle/arc segments below. Also
explicitly reject an arc/circle vertex tagged onto an isolated vertex with
no edges at all, which the old whole-mesh count also caught.

Verified live in headless Blender against the issue's repro file
(IfcFurniture "Slab.004", IfcArbitraryProfileDefWithVoids with three
IfcCircle voids): entering the profile editor, duplicating one void's
2-vertex loop and moving it produced an "INVALID PROFILE" popup before
this change, and now produces a valid profile (the original 3 voids
intact, plus the duplicate as a 4th void or a separate solid profile
depending on whether it still falls inside the outer boundary).
test/tool/test_model.py passes unchanged (32 passed, 1 pre-existing
unrelated failure present on both before and after).

Generated with the assistance of an AI coding tool.

(cherry picked from commit 0a027d47a3)
2026-07-25 23:18:32 +10:00
Petru Conduraru a8ae234039 ci-lint: black-format two files that drifted on v0.8.0
Both files were merged unformatted and fail the Black formatter step
on every branch, keeping ci-lint red repo-wide.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 6014bbd877)
2026-07-25 23:18:32 +10:00
Petru Conduraru 070b7793f4 Fix all remaining ty type-check failures on ci-lint
The ci-lint workflow's ty steps fail on every branch because base
v0.8.0 has four diagnostics.

ty check (bonsai):

- root/operator.py: bpy.data.objects.get() can return None, so
  UnlinkObject._execute could put None in its objects list and crash
  on the first attribute access when an unknown object name is passed.
  Handle the miss explicitly, which also satisfies the declared
  list[bpy.types.Object] type.
- tool/sequence.py: ty does not narrow Literal types through
  membership tests on list literals, so the assert_never() exhaustive
  check was flagged. Use tuple literals, which ty narrows, keeping the
  exhaustiveness check intact.

ty check (ios):

- draw.py: arrange_polygons was called through conditional argument
  splats that let the same call site work against pre-April-2026
  wrappers lacking arrange_polygon_settings and the logger parameter.
  No runtime bug for current builds, but the dynamic splats cannot be
  typed against the fixed 3-parameter signature. Drop the old-build
  workaround and call the current signature directly, following the
  precedent of 3d8115ebc5 which dropped similar old-build workarounds
  in ifcopenshell.file. Verified against a current wrapper build that
  the direct call arranges polygons and serializes to SVG, with and
  without a logger.
- Optimise.py: igraph is an optional dependency with a guarded import
  and a toposort fallback, but it was missing from the ios type-check
  venv so ty could not resolve it. Add it to type-check-requirements
  next to the toposort fallback that is already listed.

After this, poe ty-bonsai and poe ty-ios both pass cleanly.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 6d6d92b849)
2026-07-25 23:18:32 +10:00
Petru Conduraru 5d13791a01 Bonsai: make dxf2ifc.py example script skip unsupported DXF entities
The script called Polyline.get_mode() on every modelspace entity, but
that method only exists on POLYLINE entities, so any typical DXF
containing lines, circles or text crashed with AttributeError before
converting anything. Test for POLYLINE polyface meshes with
dxftype()/is_poly_face_mesh instead and skip other entities with a
message, only create the spatial containment relation when products
exist, and take the input/output paths from the command line (matching
obj2ifc.py) instead of a hardcoded input.dxf/test.ifc.

Fixes #2151

This change was written with the assistance of an AI coding tool.

(cherry picked from commit bb49822f2e)
2026-07-25 23:18:32 +10:00
Bruno Postle 68765828f8 Fix Bonsai polyline not enough values to unpack error
Typo was introduced in b35f99e

(cherry picked from commit 21ea58b0e6)
2026-07-25 23:18:32 +10:00
Ryan Schultz 5678fe4888 Fix #6652: Extend grab selection to include BBIM_Array members (#7968)
When grabbing an array child, the selection now expands to include
the array parent and all sibling children before the move operator
runs. Mirrors existing behavior for aggregates and nests.

Generated with the assistance of an AI coding tool.

(cherry picked from commit b66d8b2c4d)
2026-07-25 23:16:38 +10:00
Stephen Boddy 6efe9ace8d Bump build 821cf7b > e333c1c
(cherry picked from commit f9be61c10b)
2026-07-25 23:16:38 +10:00
Stephen Boddy 9dd071f9a0 Sync ifcopenshell_wrapper.pyi with sync_stub.py
Ran the new sync_stub.py against a real local build: adds
context.delete_same_facet_edge_pairs (present on the compiled wrapper,
missing from the stub) and drops the module-level logger_or_root
(present in the stub, no longer exists on the wrapper at all).

Nothing else changes - no license header rewrite, no docstring loss,
none of the 14 hand-curated named-parameter constructor/function
signatures touched, unlike the wholesale regeneration this replaces.

Generated with the assistance of an AI coding tool.

(cherry picked from commit b61f809731)
2026-07-25 23:16:38 +10:00
Stephen Boddy b61484f3a6 Add sync_stub.py, a minimal-diff stub syncer
generate_stub.py (this branch's earlier commit) regenerates
ifcopenshell_wrapper.pyi wholesale from the compiled wrapper: it
reliably fixes real drift, but it also discards everything that isn't
mechanically recoverable from the wrapper alone - the license header,
docstrings, and hand-curated named-parameter signatures for
SWIG-overloaded constructors/functions (SWIG itself always emits
generic `*args` for those, so a regenerator can't tell a deliberate
curation from real drift and just overwrites it).

sync_stub.py takes the smaller-blast-radius approach: it only adds
top-level symbols/class members that are genuinely missing, and only
removes ones that are genuinely gone, cross-checking against
validate_stub.py's own full canonicalisation (via the newly-exposed
get_names_tree()) so it never mistakes a property()/staticmethod()-
wrapped member for something absent just because its own narrower
parser skips that form. Anything that exists on both sides under the
same name but with a different signature - exactly where curation
lives - is left untouched and reported for a human to review instead
of guessed at.

Verified against a real local build: applying it to the current
ifcopenshell_wrapper.pyi produces a small, targeted diff (add one
missing method, drop one stale function) with the license header,
docstrings, and all 14 curated constructor/function signatures
preserved byte-for-byte, versus generate_stub.py's ~1000-line
wholesale rewrite for the same underlying fix.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 948ffce7e9)
2026-07-25 23:16:38 +10:00
Petru Conduraru da4127f33b Fix pythonocc-core viewer compatibility in geom.occ_utils and geom.app (#1037, #1098)
set_shape_transparency() called AIS_InteractiveContext.SetTransparency(),
whose argument count is inconsistent across pythonocc-core versions
(reported as a TypeError in #1037). Set transparency directly on the AIS
object instead, the same stable pattern already used elsewhere in this
file (display_shape() calls ais.SetTransparency() directly, never through
the Context), then call Context.UpdateCurrentViewer() to refresh.

app.py's viewer used a "SetSelectionPriority(counter)"/"SelectionPriority()"
pair as an ad hoc unique key to map a displayed AIS object back to its IFC
product. On modern pythonocc-core this crashed with AttributeError because
.GetObject() (needed to unwrap the old handle-based API) no longer exists
on AIS objects (#1098, PR #1113 partially patched one of the two call
sites but left the one in HandleSelection unguarded).

Live pythonocc-core 7.9.3 testing showed the GetObject() guard alone is
not sufficient: SetSelectionPriority/SelectionPriority themselves have
been removed from AIS_InteractiveObject entirely in modern OCCT (only
AIS_Trihedron keeps a same-named but unrelated method for datum parts),
so gating the .GetObject() call with the existing USE_OCCT_HANDLE flag
would still crash the first time a shape is selected. Verified live that
AIS objects retain correct __eq__/__hash__ (matching the underlying OCCT
instance) across separate SWIG wrapper instances, so ais_to_product is
now keyed directly by the AIS object itself, removing the dependency on
the removed OCCT API and the GetObject()/handle distinction altogether.

Verified live against pythonocc-core 7.9.3 (conda-forge) using real
AIS_Shape objects obtained from ifcopenshell.geom.occ_utils.display_shape()
and a real IFC file: reproduced both the original TypeError (#1037) and
AttributeError (#1098), confirmed both fixes resolve them, and confirmed
the ais_to_product dict lookup round trips correctly through a real
Context.Select()/SelectedInteractive() call. Could not exercise the full
Qt-embedded viewer.finished()/HandleSelection() flow end to end because
this pythonocc-core build segfaults natively when creating a second GL
context inside a Qt widget on this macOS host, a pre-existing environment
issue unrelated to this diff (reproduces identically with unpatched code,
before any touched line executes).

AI-generated, reviewed and tested by Petru Conduraru.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 6603c8459a)
2026-07-25 23:16:38 +10:00
Petru Conduraru 0191ac63dc ifcwrap: keep geometry's owning element alive to fix silent data corruption (#1124)
create_shape() returns a Python-owned Element (SWIG_POINTER_OWN in the
boost::variant out typemap). Its .geometry property calls Element::geometry(),
which returns a reference into the element's boost::shared_ptr<Representation>
_geometry member. SWIG wraps that reference as a non-owning pointer, so the
returned Triangulation/BRep/Serialization proxy does not keep the element alive.

When a caller keeps only .geometry (e.g. create_shape(s, e).geometry) and drops
the parent element, Python garbage-collects the element, destroying its
shared_ptr and freeing the underlying representation. Subsequent reads of
verts/faces then return freed memory: empty or implausible float/int garbage,
non-deterministically depending on GC and allocator timing. This is silent data
corruption, not a crash, and has bitten users since 2020.

Fix: in the TriangulationElement/SerializedElement/BRepElement pythoncode, wrap
the geometry getter so the returned geometry stores a backreference to its
owning element (result._parent = self). This makes the parent's lifetime at
least as long as the geometry's, automatically and transparently, so no caller
has to remember to hold the element. This is aothms's suggested backreference,
applied generically in the binding rather than left as a workaround.

Reproduced deterministically (washBasin fixture): before, verts len 0 vs 133500
across repeated GC-pressure runs; after, 133500 every run for all three element
types. test_create_shape passes; no regressions.

Note: tree.select_ray()'s ray_intersection_result (2024 follow-up in #1124) is a
separate ownership mechanism (std::vector element reference + std::array member
pointer) and is left as follow-up scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 824c1fc280)
2026-07-25 23:16:38 +10:00
carlopav b5fa31d762 drawing: compute cut/fill intersection once per CutDecorator object
recalculate_cut() and recalculate_fill() each ran is_intersecting_camera(),
which builds a bmesh and scans every vertex. When a redraw recalculated both
(camera moved, cache miss, or the object selected) that was two full
intersection tests per object per frame for the same answer.

Compute it once in decorate() and pass it to both, and skip the test
entirely when neither recalculation is needed. Never more tests than before,
identical result since the camera can't move within a frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 705af7ba3a)
2026-07-25 23:16:38 +10:00
carlopav 7de625b3d3 drawing: evaluate camera movement once per CutDecorator redraw
is_camera_moved() runs eval()/numpy over the camera matrix and, as a side
effect, refreshes the stored checksum the first time it returns True. It was
called up to twice per object inside decorate(), so on a frame where the
camera actually moved the first call updated the checksum and every later
call - the fill check on the same object, and both checks on all remaining
objects - then saw an already-current checksum and returned False. Only the
first object's cut got recalculated; its fill and every other element stayed
stale until something else invalidated the cache.

Evaluate it once at the top of __call__ and reuse the flag. This halves the
per-object eval overhead on the common path (viewport navigation with the
camera object stationary) and, when the camera does move, correctly
recalculates the cut and fill for every intersecting element instead of just
the first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 074fc26e8f)
2026-07-25 23:16:38 +10:00
Ryan Schultz c07f9e2c45 Bonsai: preserve occurrence geometry/material/styles when deleting a type
Deleting a type used to strip its occurrences: any that displayed the
type's mapped representation lost their geometry, and inherited material
and presentation styles were dropped too.

The no-SHIFT "Delete Type" path now bakes each occurrence's geometry,
styles, and inherited material onto the occurrence before the type is
removed:
- Refactor UnassignType's unmap logic into a reusable
  UnassignType.unassign_and_unmap(), and extend it to re-attach styled
  items (copy_deep only follows forward refs, so IfcStyledItem is lost)
  and bake down any inherited (non-owned) material.
- Add RemoveType._detach_type_material_set(): unhook the type's
  IfcMaterialLayerSet/ProfileSet association cascade-free before deletion,
  so remove_product's aggressive unassign_material never fires and the
  occurrences' layer/profile-set usages survive intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit a90064929b)
2026-07-25 23:16:38 +10:00
Ryan Schultz a45acf65e5 Bonsai: add Delete Type button to Type Attributes panel
Adds a trash button in BIM_PT_type_attributes that deletes the relating
type via bim.remove_type. SHIFT+Click also deletes every occurrence of
the type in the project, behind a confirmation dialog showing the count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 397f13e71c)
2026-07-25 23:16:38 +10:00
Petru Conduraru 6dba261e1b Optimise IfcPatch recipe: make toposort backend configurable
aothms asked for the toposort dependency ordering used by the dedup
walk to try igraph's C-backed topological_sorting() first, since it
should shave off additional time on top of the non-recursive
get_info fix. Falls back to the pure python toposort package with a
warning if igraph is not installed.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 7ebdd046b6)
2026-07-25 23:16:38 +10:00
Petru Conduraru 4299968b6e Fix #1043. Optimise IfcPatch recipe: avoid redundant recursive get_info
The 2020 profiling in issue #1043 found the Optimise recipe's dedup
loop spent almost all of its time in entity_instance.get_info(recursive=True):
because the topological sort already guarantees every referenced entity
is folded before the entity that references it, recomputing each
already-folded subtree's canonical value from scratch for every parent
that points to it is wasted work. Confirmed this is still exactly the
bottleneck in the current codebase, unchanged since 2020 (get_info's
recursive path still walks the whole subtree on every call).

Applied aothms's suggested fix from the issue thread: canonicalize each
entity with a non-recursive get_info, and for referenced entities substitute
the already-computed identity of their folded replacement (looked up in
instance_mapping) instead of re-expanding the subtree. Also limited the
toposort dependency graph to direct references (max_levels=1), since a
topological sort only needs direct edges, not the full transitive closure
traverse() was computing for every entity.

Benchmarked before and after on real IFC test fixtures and a larger
synthetic file with heavily shared geometry (thousands of walls sharing
a handful of profile/point subtrees, mirroring the sharing pattern
described in the issue):

- test/input/geometrygym_great_court_roof.ifc (56989 entities): 9.9s -> 1.7s
- test/input/acad2010_objects.ifc (16296 entities): 3.7s -> 0.4s
- synthetic 120083-entity fixture with heavy geometry sharing: 19.2s -> 3.4s

Verified correctness by comparing the full canonical (recursive get_info)
multiset of the optimized output between the old and new implementation on
all three fixtures: identical results, same fold counts.

Added test_Optimise.py covering the core scenario from the issue: entities
built from separate, value-identical non-rooted subtrees fold to a shared
instance, while entities with distinct values do not.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 57cfd9d1fd)
2026-07-25 23:16:38 +10:00
Petru Conduraru 634a7add85 ifcgeom: build the swept-area directrix from the offset curve far from origin (#4848)
IfcSurfaceCurveSweptAreaSolid regressed in 0.8 for geometry far from the
origin (for example parapets on a georeferenced building), which went
missing or glitched.

The kernel offsets the directrix toward the origin when it is far away
(mean.norm() > 1e2), storing the offset copy in a local curve variable and
setting applied_temporary_offset so the finished solid is translated back by
+mean. But the wire was still built from scs->curve, the un-offset original,
so the offset never took effect and the result was translated by +mean from
its correct location. Build the wire from curve instead. When no offset is
applied curve aliases scs->curve, so near-origin geometry is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit e333c1c100)
2026-07-25 23:16:38 +10:00
Bruno Postle 57d9c47c6c Fix null-pointer derefs in reference resolution
Two related bugs in read_from_stream's reference-resolution
loop, both reachable from malformed input:

- has_attribute_value<IfcBaseClass*> only checks the stored
  slot's type, not that it's non-null (e.g. an explicit $
  value), so the following get_attribute_value() call could
  return null and inst->declaration() crashed on it.
- byid_[ref] default-inserts (and returns) a null pointer
  when the owning instance id isn't present, which was then
  dereferenced unconditionally via ->data().

Added regression tests using the two minimized crash inputs
that found these.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 56121ca061)
2026-07-25 23:16:38 +10:00
Bartok 141cf95568 docs(readme): use https for IfcOpenShell website link
(cherry picked from commit 7b613a0bcc)
2026-07-25 23:16:38 +10:00
Andrej730 6858cccdcc settings_mixin.build_parser: fix ty == "bool" typo, should be an assignment
(cherry picked from commit f744753726)
2026-07-25 23:16:38 +10:00
Andrej730 982f5e802c assign_cost_item_quantity: fix indendation and missing values (de65e50)
`values` dictionary was missing and variables were never collected to it, so `FormulaEvaluator(values)` was always resulting in missing variable error.

(cherry picked from commit 2e21fc5a98)
2026-07-25 23:16:38 +10:00
Andrej730 b1f7e44175 assign_cost_item_quantity: annotate
(cherry picked from commit ca9bbbc4a7)
2026-07-25 23:16:38 +10:00
Andrej730 1a2e6872be edit_true_north: handle unsetting case when TrueNorth is already None
(cherry picked from commit 47dc1a6c68)
2026-07-25 23:16:38 +10:00
Stephen Boddy c17ea80827 Remove stale ty lint ignore directive
(cherry picked from commit 489084c7be)
2026-07-25 23:16:38 +10:00
Stephen Boddy bb908f3dfa Route boolean-op kernel logging through the injected logger
Ports #96e2efebc onto wgpu. wgpu's boolean_utils logged via the global
::logger::root() singleton (which IfcConvert never wires to --log-file),
so boolean-op messages were effectively dropped. Thread the caller's
injected logger through instead:
- boolean_settings gains `::logger* logger` + `log()` accessor (falls
  back to ::logger::root()); boolean_operation logs via settings.log()
- eliminate_narrow_operands / boolean_subtraction_2d_using_builder take a
  `::logger& logger = ::logger::root()` param; boolean_operation passes
  settings.log() into them
- OpenCascadeKernel / boolean_result set bst.logger = &logger_ and log
  via logger_ (were ::logger::root())
Adapted from v0.8.0's Logger/Logger::Root() to wgpu's ::logger/::logger::root().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:16:38 +10:00
Stephen Boddy 60b4f6191c Allow process/resource type assignment via Type-suffix convention
The class-pairing validation added in 10ee5aef4f rejects any type
assignment whose class isn't in the buildingSMART implementer
agreement map. That map only covers physical product occurrence/type
pairs (IfcWallType -> IfcWall, etc); IfcTypeProcess and IfcTypeResource
subtypes such as IfcTaskType, IfcProcedureType and the resource types
have no entry, so previously-valid assignments like
IfcTaskType -> IfcTask were rejected with "allowed occurrence
classes: <none>".

These classes still follow the schema's universal Type-suffix naming
convention, so derive the pairing the same way the existing
ApplicableOccurrence fallback does: strip "Type" from the relating
type's class name and accept it only if the schema actually declares
that entity. This can only add pairings implied by the type's own
class name, so it cannot loosen the existing rejection of genuine
mismatches (e.g. IfcWallType -> IfcWindow).

Generated with the assistance of an AI coding tool.

(cherry picked from commit d188e3beaf)
2026-07-25 23:16:38 +10:00
Dion Moult a341ad29f3 port: SVG edge classification (#3668) onto wgpu [worklist #114-120]
Ports the 7-commit v0.8.0 SVG edge-classification feature (f0970b90b +
6 follow-ups) onto wgpu's heavily-diverged serializer. Reconstructed
block-by-block rather than merged, because both sides rewrote
SvgSerializer (v0.8.0 +419, wgpu +778) and git's conflict alignment was
misleading.

Key wgpu adaptations reasoned per block:
- IfcUtil::IfcBaseEntity* (pointer identity) -> express::Base (value),
  incl. as a std::map key in draw_hlr (express::Base has operator<);
  nullptr fallback -> express::Base{}
- boost::optional -> std::optional (css_class, dash_array)
- hlr_calc::result_type pair -> 3-tuple (adds per-edge class label)
- draw_hlr restructured with a group_by_product map: ONE path_object per
  product so multiple class buckets share a group and per-path classes
  survive Bonsai's merge (NOT naive per-item threading, which fragments
  groups -- caught during visual verification)
- settings wired into wgpu's apply_settings() (ctor-called), NOT the
  feature's added ready() call which wgpu already solved differently
- logger_.Warning -> logger().warning in write(geometry_data)

Verified end-to-end via IfcConvert on a curved-geometry stress scene
(Suzanne/Torus/Sphere/Cube/Cone/...): edges classify into outline/sharp/
crease/boundary/flush with correct per-product grouping and CSS styling.
Requires the model's storey to carry an Elevation and --svg-project to
trigger the HLR projection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:16:38 +10:00
Ryan Schultz 43fc405b58 Bonsai: allow cross-family class reassignment for spatial elements with geometry (#8665)
The Reassign Class operator refused to reassign an element to a different
IFC product family unless it was an IfcElement <-> IfcElementType swap, so a
piece of geometry mistakenly hosted on IfcSite could not be turned into
IfcFurniture even though root.reassign_class handles it fine.

Loosen the guard: only block the case that actually matters - a spatial
element (IfcSpatialElement / IfcSpatialStructureElement for IFC2X3) with no
geometry, which would be a real containment-hierarchy container rather than
a stray modelled object. Everything else reassigns freely.

Closes #8664

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit b5a0f1fc74)
2026-07-25 23:16:38 +10:00
Petru Conduraru dbbbc54f19 Bonsai: make 'has openings' representation error actionable (#8108)
When converting a wall representation to a parametric extrusion via the
Representation Utilities buttons, an element that has openings would report
"has openings - representation cannot be updated" and stop, without telling
the user there is an ALT+click path that bakes the openings into the new
representation. Point the message at that path so the error is actionable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 25441bd816)
2026-07-25 23:16:38 +10:00
Petru Conduraru f76ee61812 Bonsai: place auto-generated opening boundaries at their real position (#8237) (#8311)
* Bonsai: place auto-generated opening boundaries at their real position #8237

auto_generate_boundaries (single-space mode) built each opening/filling boundary
from the opening's LOCAL geometry (get_vertices) but first did
mat.translation = (0, 0, 0) on its placement matrix. Because the vertices are
local, that placement translation is exactly what carries the opening to its
real location, so zeroing it collapsed every window/door boundary onto the
origin. This is why the auto path misplaced window boundaries while the
single-element path (create_element_boundary) placed them correctly, as
@MDHering observed with the two modes. Keep the full placement matrix.

Verified on the reporter's file: the opening's real placement is (0.1, 1.5, 1.0);
a vertex went from (0.6, 0, 0) under the old code to (0.7, 1.5, 1.0) with the fix,
i.e. moved by exactly the (0.1, 1.5, 1.0) that was being discarded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remove superfluous comment from #8237 fix

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: CyrilWaechter <cyril@biminsight.ch>
(cherry picked from commit 65811ac7c9)
2026-07-25 23:16:38 +10:00
Andrej730 a55ce7b1fb test-package: drop stale comment
This information is already documented in maintanence.rst.

(cherry picked from commit d9d1824886)
2026-07-25 23:16:38 +10:00
Andrej730 6997046743 Bump build 3e7b739 -> 821cf7b
Just to test everything is working with the changes from the last month.

(cherry picked from commit 16e5f18553)
2026-07-25 23:16:38 +10:00
Andrej730 a62238bcaf test-package: assert BUILD_COMMIT is a 7-char short SHA
(cherry picked from commit b7a9b7bc5a)
2026-07-25 23:16:38 +10:00
Andrej730 ddbd3adf40 test-package: verify build URLs with HEAD requests instead of scraping listing page
(cherry picked from commit e14397058d)
2026-07-25 23:16:38 +10:00
Andrej730 90dfd2926e stub: add missing entity.inverse_attributes
(cherry picked from commit 9123d8c183)
2026-07-25 23:15:23 +10:00
Andrej730 725162823c ci-lint: run ty-bonsai and ty-ios as separate steps
So if one fails, it wouldn't block another.
Noticed by Stephen in d5e890bccd

(cherry picked from commit ffd939508c)
2026-07-25 23:15:23 +10:00
Andrej730 a0556c1124 build-all: ensure all patches are present
Also changed type to just `list[str]` to keep it simple.

(cherry picked from commit 816eba5145)
2026-07-25 23:15:23 +10:00
Andrej730 641becb4e0 build-all: drop unused opencollada pr622 patch
Last reference to this file was dropped in 7ae685dbf, though the ref was
pointing to `/patches/opencollada/pr622.patch`, so IIUC
`patches/pr622.patch` was never used.

(cherry picked from commit c013b9aca7)
2026-07-25 23:15:23 +10:00
Andrej730 647f53163c build-all: drop unused occt patch
Introduced in e21277e80, reference removed
in 683cadeb7 when occt was bumped to 7.3.0 and switched to git-tag based
download.

(cherry picked from commit 24e454ce0c)
2026-07-25 23:15:23 +10:00
Andrej730 c3c119d8e0 pyproject: add nix script to ty check
(cherry picked from commit cb497b37f7)
2026-07-25 23:15:23 +10:00
Andrej730 4f429f60f9 ifcclash: fix use of undefined clash["position"]
It's an artifact from the old hppfcl clasher dropped in 18c38b312

(cherry picked from commit 71c6950a59)
2026-07-25 23:15:23 +10:00
Andrej730 30c9956fac bsdd: fix test_get_class_relations
`classRelations` doesn't exist on `ClassPropertiesContractV1`, probably was just a typo.

(cherry picked from commit a7a7edfd27)
2026-07-25 23:15:23 +10:00
Andrej730 6935c631d4 build-all: fix note about the schemas built by default
(cherry picked from commit 5273569b08)
2026-07-25 23:15:23 +10:00
Andrej730 39d6459511 misc: more readable poll error for import_quick_favorites
(cherry picked from commit 78712ead98)
2026-07-25 23:15:23 +10:00
Andrej730 8f12a15678 surveyor: drop never used dead code
Surveyor test was failing because `get_z_rotation` and `set_z_rotation` were not implemented.
The code was added in 230cbe1fd8, but it was never used.

(cherry picked from commit 9e25c12b16)
2026-07-25 23:15:23 +10:00
Andrej730 c30c1d3f69 Deduplicate code by reusing tool.document
(cherry picked from commit 0968d06780)
2026-07-25 23:15:23 +10:00
Andrej730 136ae52c8e file.get_inverse: document with_attribute_indices overload
(cherry picked from commit 1b1da821f1)
2026-07-25 23:15:23 +10:00
Andrej730 b30bd5b4f2 geometry.add_boolean: fix typo in the class name
🫣🫣

(cherry picked from commit d772b24bd6)
2026-07-25 23:15:23 +10:00
Andrej730 672b0f81f4 ios pyproject: add networkx stubs as dev dependency
(cherry picked from commit 549f81a76e)
2026-07-25 23:15:23 +10:00
Andrej730 2f4bcc17a6 geom/main.py: fix ty complaint
(cherry picked from commit 8a00ce84cc)
2026-07-25 23:15:23 +10:00
Dion Moult 98cfc7873c port: fix XmlSerializer property_set_sets type (aggregate_of -> auto)
My resolution of #25 (e38993909) over-took v0.8.0's explicit
aggregate_of<...>::ptr type, which the rewrite renamed. Every other
get_related call in this file uses auto (it's a deduced-return template);
wgpu's HEAD already used auto here. Only the SCHEMA_HAS_ macro typo fix
was actually needed. Revert the type to auto.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:15:23 +10:00
Andrej730 94b95527ea maintenance.rst: move pyver matrix to bundled Python version section
(cherry picked from commit 3e9ef82448)
2026-07-25 23:15:23 +10:00
Andrej730 4c896954bb ci-bonsai-daily: Use Blender 5.2 for tests
(cherry picked from commit a5c77fd096)
2026-07-25 23:15:23 +10:00
Andrej730 2a14e2e29c pyproject: support formatting with ruff
Since it's black-compatible drop-in replacement and they can be used
almost interchangeably.

(cherry picked from commit d183961280)
2026-07-25 23:15:23 +10:00
Andrej730 a6a6c20a18 dev_environment.py: add shebang and make executable
(cherry picked from commit 97d1a6e488)
2026-07-25 23:15:23 +10:00
Petru Conduraru 73687e041e Bonsai docs: fix version switcher scheme mismatch (http vs https)
versionURLs in brand.html used http:// while the docs sites are
served over https://, so currentURL.includes(url) never matched and
the <select> never reflected/switched to Unstable. Fixes #8023.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 780739719f)
2026-07-25 23:15:23 +10:00
Petru Conduraru ff244cc2db Fix ci-bonsai-daily: reconnect Cost/IfcGit tool interfaces (TestImplementsTool)
Two TestImplementsTool failures on v0.8.0:

- test_cost.py: Cost could not be instantiated because
  core.tool.Cost declared abstract get_direct_cost_item_products, which
  tool.cost.Cost never implements. The method is dead (zero call sites;
  get_cost_item_products(is_deep=False) already covers the 'direct'
  case), so remove the abstract declaration.
- test_ifcgit.py: tool.ifcgit.IfcGit was not declared as a subclass of
  its core.tool.IfcGit interface (unlike every sibling tool class), so
  the isinstance check failed. Add the base class (and the
  bonsai.core.tool import it needs). All 50 interface methods are
  already implemented on the concrete class.

No behaviour change. Verified in headless Blender: isinstance(Cost(), core.tool.Cost) and isinstance(IfcGit(), core.tool.IfcGit) both True (were TypeError / False); repo abstract-vs-impl diff confirms all IfcGit abstracts are implemented.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 4a717ca7ff)
2026-07-25 23:15:23 +10:00
Petru Conduraru cdd54917bc Fix ci-lint: black-format selector.py
black (the version CI's psf/black@stable resolves to) flags three spots
in util/selector.py: the chained .replace() in FormatTransformer.number,
the suppress_zero_inches kwarg in format_length, and the long
`elif key in (...) and hasattr(...)` placement-key tuple in
set_element_value. Reformat all three to black's multi-line style.
Formatting only, no behavioural change (all keys preserved).

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 5a831e3d21)
2026-07-25 23:15:23 +10:00
Petru Conduraru c2b168b45b Bonsai: deterministic annotation order in generated drawing SVGs (#6608)
generate_annotation built the annotation list from a set union and sorted it by
ZIndex and TEXT-ness only. Annotations that tied on that key kept set iteration
order, which follows entity hash (step id plus the process memory address), so
the order of tied annotations (for example a label and its background fill)
shuffled between Blender restarts and flipped their draw order.

Add the stable IFC step id as a final tiebreaker so the order is total and
session independent. Behavior preserving, no z-layer semantics changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit d30286225c)
2026-07-25 23:15:23 +10:00
Bruno Postle 3203148299 ifcedit: fix Optional[entity_instance] coercion crash on native JSON values
coerce_value assumed value_str was always a CLI string, but ifcmcp
passes JSON-decoded native types (int, None) straight through. Guard
the Union/Optional "none" check so it only calls .lower() on strings,
and handle native None explicitly.

(cherry picked from commit 65695fb878)
2026-07-25 23:15:23 +10:00
Bruno Postle aef141505b ifcedit: include IfcSpace in default QTO element scope
IfcSpace is not a subtype of IfcElement, so quantify.run_quantify()'s
default selector silently skipped all spaces, reporting
elements_quantified: 0 with no error or warning.

Generated with the assistance of an AI coding tool.

(cherry picked from commit ab15750747)
2026-07-25 23:15:23 +10:00
Petru Conduraru 8d9f027f3e ifc4d: tolerate activities without a CalendarObjectId in P6 import (#5617)
Importing a Primavera P6 XML crashed with
`AttributeError: 'NoneType' object has no attribute 'text'` in
P62Ifc.parse_activity_xml, which read
activity.find("pr:CalendarObjectId").text unconditionally. CalendarObjectId
is optional on a P6 Activity; when omitted, the activity inherits the
project's ActivityDefaultCalendarObjectId.

Capture the project default in parse_xml and fall back to it when an
activity has no CalendarObjectId (`calendar_id or self.default_calendar_id`).

Verified on the reporter's attached file (20241021 Cronograma.xml): 3 of 14
activities lack a CalendarObjectId and reproduced the exact crash on
v0.8.0; after the fix parse_xml completes and those activities resolve to
the project default calendar "2" (a valid calendar in the file). An
activity with an explicit CalendarObjectId keeps its own value.

Fixes the P6 re-import crash reported in #5617 (that issue tracks several
Gantt items; this addresses the import AttributeError).

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 694a44e638)
2026-07-25 23:15:23 +10:00
Petru Conduraru c4609a634c util.element: read property sets inside an IfcPropertySetDefinitionSet (#6330)
get_pset and get_psets assumed RelatingPropertyDefinition is a single property
definition and read definition.Name directly. When it is an
IfcPropertySetDefinitionSet (a defined type wrapping a list of property set
definitions) that attribute access raised AttributeError, so an element whose
psets are grouped in a set returned none of them.

Unpack IfcPropertySetDefinitionSet into its members in both loops and process
each one. Single property definitions and the psets_only and qtos_only filters
are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit a3950ac191)
2026-07-25 23:15:23 +10:00
Petru Conduraru af762ca810 Fix ci-bonsai-daily: renumber stale STEP ids in BDD feature fixtures
Several BDD scenarios hardcode absolute representation-item object names
whose trailing number is the IFC STEP line id
(f"Item/{item.is_a()}/{item.id()}"). Those ids drift when file-creation
order changes; a recent shift moved all of them by a uniform -4, so the
scenarios failed with "Item/.../NN does not exist".

The failing step (the_object_name_exists in test_feature.py) dumps the
full bpy.data.objects listing on failure, so the correct current ids are
recoverable directly from the CI log (run 29208793599, tested commit
36e21e882f, an ancestor of HEAD with only a .gitignore commit between).
Renumber to match:
  IfcExtrudedAreaSolid/77->73, IfcPolygonalFaceSet/76->72,
  IfcVertexPoint/69->65, IfcEdge/72->68, IfcFace/74->70.

Verified against the CI failure dump (a local build produces different
ids, so this is validated by CI's own object listing rather than a local
run). boolean.feature also hardcodes IfcHalfSpaceSolid/90 and panel text
[91] downstream of the failing assertion, which CI never reached and so
never dumped; left as-is to avoid guessing - they will print a fresh dump
next run for a follow-up if still stale.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 6f90badda8)
2026-07-25 23:15:23 +10:00
Petru Conduraru c3469a5da2 docker: fix GID collision and macOS sed portability
Two host-environment bugs in the build-env scripts that break on
macOS/Apple Silicon hosts, independent of target architecture:

- Dockerfile: groupadd fails outright when USER_GID collides with an
  existing system group in the rockylinux9 base image (e.g. macOS
  default user GID 20 "staff" collides with RHEL's GID 20 "games").
  Guard with getent so useradd attaches to the existing group instead.
- ifcos_env: `sed -si` is GNU-only syntax and errors under BSD/macOS
  sed. Do the UNIQUE_ID substitution via a portable temp-file + mv.

Per sboddy's review on the original PR: dropped the linux/amd64
platform-pin additions from this change. The stack already targets
Rocky9/x64 build outputs by design, and Docker Desktop on macOS has
no native container runtime regardless (it's a Linux VM either way),
so forcing the image to run under emulation doesn't produce anything
that's actually loadable into a native macOS Blender/Bonsai install.
That's a separate, harder problem worth solving via a native build
path instead (mirroring build_osx.yml), not by fighting emulation
here. These two fixes stand on their own merits on any host.

This change was made with the assistance of an AI tool.

(cherry picked from commit 8b05510d6c)
2026-07-25 23:15:23 +10:00
Stephen Boddy eadd12cad4 Share ccache volume across checkouts, cap at 2G
The ccache named volume had no explicit name, so Docker Compose
namespaced it under the per-checkout project name (derived from
UNIQUE_ID), giving each checkout its own cache even though
docker/README.md already documented them as shared. Give the volume
a fixed name so all checkouts attach the same one.

Measured cache size after a full build (IfcParse+IfcGeom+IfcConvert+
wrapper, one Python version) is ~300MB, only ~5% of the previous 5G
cap. Shrink CCACHE_MAXSIZE to 2G, which comfortably covers the shared
baseline plus per-branch deltas from several diverging checkouts.

Generated with the assistance of an AI coding tool.

(cherry picked from commit b1470223d3)
2026-07-25 23:15:23 +10:00
Petru Conduraru 4f0e1f718d docker: make the build env work on macOS / Apple Silicon hosts
Three host-portability fixes to the docker/ toolchain from #8564 so it
runs on macOS as well as Linux. All three are no-ops on native amd64
Linux.

1. Dockerfile: only groupadd when the target GID is free. macOS's default
   primary group `staff` is GID 20, which already exists as `games` in
   rockylinux:9, so `groupadd -g 20` aborted the image build. Guard with
   `getent group "${USER_GID}" || groupadd ...`; useradd -g accepts the
   existing GID.

2. ifcos_env unique(): replace GNU-only `sed -si` (BSD/macOS sed errors
   "illegal option -- s") with a portable `sed > tmp && mv` rewrite of the
   UNIQUE_ID line. Verified against macOS BSD sed.

3. create() + compose.yaml: build with an explicit `--platform linux/amd64`
   so the locally built image's platform matches the `platform:
   linux/amd64` pin in compose.yaml. Without it, on arm64 the local image
   is tagged linux/arm64, compose treats the platform-mismatched image as
   absent and tries to pull `ifcopenshell-build-env:updated` from Docker
   Hub (which does not exist -> access denied). Also add `pull_policy:
   never` as a safety net so a future mismatch surfaces as a clear "image
   not found" rather than a registry auth error.

Note: on Apple Silicon the amd64 build runs under emulation and a cold
full build is slow; ccache makes incremental rebuilds tolerable. A native
Linux/Intel host or CI remains the better choice for routine use, but these
fixes turn "hard broken" into "works with a caveat" on macOS.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit f25b072fa0)
2026-07-25 23:15:23 +10:00
sboddy 0ee1bc7547 Add .gitignore entries for docker build env (#8569)
(cherry picked from commit ffb867f254)
2026-07-25 23:15:23 +10:00
Stephen Boddy 02aa60b135 Fix segfault closing autosave recovery dialog
Reported: Blender segfaults when clicking Cancel on the "newer
autosave found" recovery popup shown by LoadProject at startup.

Root cause: LoadProject.execute()/invoke() triggered the recovery
popup via bpy.ops.bim.load_autosaved_recovery_popup("INVOKE_DEFAULT",
...) and returned that call's result ({'RUNNING_MODAL'}) as their own
return value, without LoadProject itself ever calling
modal_handler_add(). Blender's window manager takes a RUNNING_MODAL
return as a promise the operator registered its own modal handler;
since it hadn't, the WM's operator bookkeeping was left corrupted -
silently, since this is heap/state corruption rather than an
immediate crash. It only surfaced later, when the real modal operator
(the popup) closed and the WM reconciled its modal stack, which lines
up with the crash occurring specifically on dialog close regardless
of which button was pressed. check_autosave_recovery() now returns a
plain bool and fires the popup fire-and-forget; LoadProject reports
its own honest {"FINISHED"}.

Also hardened, as defense in depth: LoadAutosavedRecoveryPopup's
execute()/cancel() call back into bim.load_project(...), which (with
should_start_fresh_session) calls wm.read_homefile() and tears down
the window manager/screens. Doing that synchronously from inside this
popup's own execute()/cancel() - itself invoked from deep inside
Blender's modal handling for the popup's button click - risks the
same class of use-after-free as the timer bug fixed in the previous
commit. The reload is now deferred by one timer tick so it runs after
the popup's modal handling has fully unwound, and the deferred
callback closes over plain values rather than `self`, since the
operator instance may not survive past cancel()/execute() returning.

This defer-only change was tried and tested first, on the (incorrect)
assumption it was the root cause: it produced a byte-for-byte
identical crash backtrace on retest, which is what pointed at the
RUNNING_MODAL bug above as the actual cause - the defer change alone
was insufficient because the corruption happens when the popup is
first shown, not when it's closed.

Generated with the assistance of an AI coding tool.

(cherry picked from commit d0eca6fa90)
2026-07-25 23:15:23 +10:00
Stephen Boddy f24e637dc9 Fix autosave timer self-unregister crash risk
The periodic autosave timer called reset_timer() at the end of its
own callback, which unregistered the timer that was still executing
(itself). Blender frees the timer's internal registry entry on that
manual unregister, then frees it again when the callback returns
None - a double free that corrupts the heap and can crash Blender
later, once the corrupted memory is reused.

Reschedule by returning the next interval from the callback instead,
which is the safe, documented way to repeat a bpy.app.timers
callback. External reset_timer() calls (from SaveProject,
LoadProject, AutosavePrompt) are unaffected since they run from a
separate call stack (UI events), not from inside the timer.

Found while investigating a segfault reported when cancelling the
autosave recovery popup; not itself the cause of that crash (see the
following commit), but the same reentrant-unregister pattern and a
real, independent latent bug in the periodic reminder path.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 6306ce0f80)
2026-07-25 23:15:23 +10:00
Stephen Boddy 316dace11a Harden docker build tooling: non-root, clean lifecycle, try()
Dockerfile (renamed from Dockerfile_init, Dockerfile_update removed):
- Run as a non-root `builder` user matching the host UID/GID (passed as
  --build-arg by create() from id -u/id -g), so build output under the
  bind mount stays owned by the host user instead of root.
- Fix CCACHE_MAXSIZE: `ccache -M 5G` wrote its limit to a config file
  under /ccache at image-build time, but /ccache is a volume mount
  point, so that file gets shadowed by the (empty) volume the moment
  the container actually runs - the cap never took effect. Set
  CCACHE_MAXSIZE=5G as an image ENV instead.
- Dedupe ccache/libffi-devel, add --setopt=install_weak_deps=False
  --setopt=tsflags=nodocs, add `git lfs install --system`, combine the
  dnf update+install into one layer.
- Drop Dockerfile_update: it built FROM its own previous output, so
  every `update` call made the image strictly larger forever (Docker
  layers are append-only, `dnf clean` in a later layer can't shrink an
  earlier one). `update` now just calls create(), which already runs
  `dnf update -y` FROM a clean rockylinux:9 every time.

compose.yaml: pin platform: linux/amd64 so this doesn't silently run
under emulation on an ARM host.

ifcos_env:
- Split the previously-conflated stop/down into six distinct,
  Compose-native lifecycle commands: up (create-or-start), down
  (remove), stop, start, restart (stop+start, same container),
  recreate (down+up, fresh container). Previously `stop` was aliased
  to `down`, which silently removed the container instead of pausing
  it.
- Implement try(): copies the built wrapper into a real Blender/Bonsai
  install for manual testing, reading the target from a new
  BLENDER_USER_RESOURCE .env variable and auto-detecting the built
  Python version (disambiguating via PY_TGT for multi-version builds).
  Deliberately kept human-only - it mutates a live Blender install, so
  it shouldn't run unattended as part of an automated/AI workflow,
  which should instead copy the wrapper into the repo's own
  src/ifcopenshell-python/ifcopenshell/ (documented in SKILL.md).
- Fix unique(): the "has .env already got a UNIQUE_ID line" check
  referenced an unset $FILE instead of $ENV_FILE, so it always
  evaluated true and appended a fresh "UNIQUE_ID=dummy" line to .env
  on every single `up`.
- Minor: differentiate remove()'s log message from down()'s (no longer
  identical now that they're distinct operations), tidy help text
  alignment and a stray double-space typo in clean().

SKILL.md: rewritten as current-state documentation (no more "fixed in
this copy" changelog framing) covering the above, plus a migration
note for anyone hitting root-owned leftovers from an older image.

Verified by actually building the image and driving every new
lifecycle command (stop/start/restart keep the same container ID;
down+up and recreate produce a new one) and try() (including the
quoted-tilde BLENDER_USER_RESOURCE edge case) against the real container.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 92c50ed3b4)
2026-07-25 23:15:23 +10:00
Stephen Boddy 68b234f59b First docker build environment
First functional version, but it needs some improvements and fixes
identified as I've used it personally on one thing, and when an AI
(Claude) used it to work through the CI test errors.

I had the AI make a SKILL.md file. If the AI indicates it needs to
build the ifcopenshell binary, use this and let it rip.

(cherry picked from commit fa98aad469)
2026-07-25 23:15:23 +10:00
Petru Conduraru bda66c9ec2 Bonsai: fix KeyError in format_distance for kilometre and mile units #8255
The project-unit to Blender-unit mapping in format_distance only knew
FOOT/INCH/METRE/DECIMETRE/CENTIMETRE/MILLIMETRE, so creating a project
with Kilometers or Miles in the New Project Wizard crashed with
KeyError: 'KILOMETRE' (or 'MILE') as soon as the spatial tree formatted
an elevation. Add the missing Blender-supported units (kilometre, mile,
micrometre) and fall through gracefully for anything else (for example
HECTOMETRE) so unknown units use the adaptive formatting branch instead
of raising.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 980988f208)
2026-07-25 23:15:23 +10:00
Petru Conduraru 6d0809c703 Selector: add rotation_x/y/z value keys #6262
Expose the Euler rotation of an element's placement in degrees through
get_element_value, alongside the existing x/y/z and easting/northing/
elevation keys. This makes element rotation exportable through ifccsv,
e.g. for placing oriented symbols in GIS.

Adopts the approach agreed in the review of the stale PR #6272 by
@TZwielehner: reuse util.shape_builder.np_matrix_to_euler and do the
degree conversion inside get_element_value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit d4805387ef)
2026-07-25 23:15:23 +10:00
Petru Conduraru 21b5fa6b15 Bonsai: fall back to adaptive units for unsupported SI prefixes #8074
Project loading set scene length_unit to f"{Prefix}METERS", but Blender's
enum only defines KILOMETERS, CENTIMETERS, MILLIMETERS and MICROMETERS.
A model with a DECIMETRE (or HECTO/DECA/etc.) length unit therefore raised
on the enum assignment and the file failed to open. Guard with the set of
supported values and fall back to ADAPTIVE display for the rest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 69a4be68e8)
2026-07-25 23:15:23 +10:00
Petru Conduraru bc363e6739 docs: remove TODO placeholder sections from the create-model quickstart #8208
The quickstart ended with three empty sections whose bodies were only
"TODO" (placing occurrences, changing locations, modeling a building),
which read as a dead end on docs.bonsaibim.org. The page now ends on the
completed save-and-view flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 06da416b8f)
2026-07-25 23:15:23 +10:00
Petru Conduraru 17146d3572 resource.assign_resource: fix typo in duplicate guard #8203
The guard that avoids re-assigning the same object to the same resource
tested is_a("IfclRelAssignsToResource") (stray "l"), so it never matched.
A repeat assignment therefore fell through and appended the related object
to RelatedObjects a second time. Corrected to "IfcRelAssignsToResource".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 21ae78fbc2)
2026-07-25 23:15:23 +10:00
Petru Conduraru b3dc0b4478 fix(ifcdiff): check attributes by default so PredefinedType changes are caught (#8214)
IfcDiff defaulted to relationships=["geometry"], so a plain diff only ever
compared geometry. Attribute-only edits on an element that kept its GlobalId
(a modified or removed PredefinedType, a renamed element, etc.) were silently
missed. The CLI made this worse: --relationships did not list "attributes" or
"geometry" as valid values, so there was no documented way to enable it.

The default is now ["attributes", "geometry"], so a plain `ifcdiff old new`
reports attribute changes alongside geometry changes. The CLI help and the
IfcDiff docstring now document all valid relationship values.

Added a regression test covering a PredefinedType change detected with the
default configuration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 0a8ae14789)
2026-07-25 23:15:23 +10:00
Petru Conduraru ff798bc989 fix(selector): round() should not crash on non-numeric values (#6776)
FormatTransformer.round() called Decimal() directly on the input value,
which raises decimal.InvalidOperation when the value is a non-numeric
string (a text property, or a value carrying a unit suffix like "12.5 m").
In a spreadsheet export this crashed the entire operation as soon as one
element carried such a value.

Now round() catches InvalidOperation and returns the value unchanged, the
same graceful-fallback convention used by add(). Numeric rounding is
unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 2eea7728d2)
2026-07-25 23:15:23 +10:00
Petru Conduraru bd5ea1b039 ifcfm: convert COBie Coordinate space points to project units (#5926)
In the cobie24 Coordinate sheet, Floor rows use get_local_placement, whose values
are in the project length unit, but Space rows come from ifcopenshell.geom
create_shape, whose vertices are in SI metres, and the space branch never scaled
them back. So on a non metre model (for example millimetres) the Coordinate sheet
mixed units a thousandfold apart and disagreed with the Facility sheet's declared
LinearUnits.

Scale the space bounding box by the project unit scale so the whole Coordinate
sheet is consistent. A metre model is unchanged since the scale is 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 6b3cc54afc)
2026-07-25 23:15:23 +10:00
Petru Conduraru 007573cea5 Support block comments in selector filter syntax (#5023)
The filter_elements selector grammar had no way to comment out part of a
query, so users had to delete and retype text to temporarily toggle a
facet. Add a /* ... */ block comment terminal that is ignored by the
lexer, and tolerate a trailing "+" so that commenting out the final
operand (e.g. "IfcWall + /* IfcSlab */") parses cleanly. Comments may
span multiple lines; a /* sequence inside a quoted string is not treated
as a comment. Only the filter grammar is affected, not get_element or
format which use "/" for regex and division.

Adds a regression test and documents the syntax.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 5c11946470)
2026-07-25 23:15:23 +10:00
Ryan Schultz d39b1c56f8 Docs: clarify immediate vs. any-depth spatial selectors
The location and parent filters both match at any depth in the spatial
hierarchy, which surprises users who want only the elements immediately
under a given container. Document that the parent query key resolves the
direct parent only (e.g. query:"parent.Name"="My Site"), add a matching
filter example, and note the immediacy on the parent value key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 0b7e25a3ef)
2026-07-25 23:15:23 +10:00
Ryan Schultz 2737854372 Add bulk-load of selected drawings' annotations (#8525)
SHIFT+CTRL+CLICK on Activate Drawing now imports the
annotations of all selected drawings without switching
the active view or camera, then selects their cameras with
the first as active. SHIFT+CTRL+ALT+CLICK also selects the
loaded annotation objects. The drawing camera is imported
when missing so annotations land in the correct collection.
Loading is idempotent.

Generated with the assistance of an AI coding tool.

(cherry picked from commit d16c283aef)
2026-07-25 23:15:23 +10:00
Petru Conduraru de64f8e0a2 IfcConvert: report an error when the output file cannot be opened (#438)
Converting to a path whose directory does not exist (or is not writable)
failed silently: the serializer's ready() check correctly returned false,
but IfcConvert deleted the temp file and returned EXIT_FAILURE without any
message, so the user saw no reason for the failure.

Log a SYS error naming the output file before returning, matching the
existing "Unable to open output file" reporting used elsewhere.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit a0f493b471)
2026-07-25 23:15:23 +10:00
Petru Conduraru 29a3514aff 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
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>
(cherry picked from commit e389939092)
2026-07-25 23:15:23 +10:00
Petru Conduraru aaeca366f6 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>
(cherry picked from commit 380675e214)
2026-07-25 23:15:23 +10:00
Dion Moult 5164d7dac7 port: adapt cherry-picked ifcgeom code to wgpu APIs
Fixes clean-but-broken breakage from replayed v0.8.0 commits that
compiled on v0.8.0's API but not wgpu's renamed one (caught by the
checkpoint build, not by any merge conflict):

- face.cpp: logger().Warning -> warning (from #527)
- IfcAsymmetricIShapeProfileDef.cpp (from #1367): map_impl takes a
  reference not a pointer (matches wgpu's BIND convention); inst-> -> inst.;
  boost get_value_or -> std::optional value_or; logger_.Message/Logger:: ->
  message/::logger::
- IfcTriangulatedFaceSet.cpp: inst->PnIndex() -> inst. (my own port slip;
  wgpu's triangulated map_impl is also a reference)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:15:22 +10:00
Petru Conduraru 956881bb99 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>
(cherry picked from commit 3e55c5126c)
2026-07-25 23:15:22 +10:00
Petru Conduraru c57da082a5 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>
(cherry picked from commit 0d70812641)
2026-07-25 23:15:22 +10:00
Petru Conduraru b774c9ad18 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>
(cherry picked from commit dd9fa65629)
2026-07-25 23:15:22 +10:00
Petru Conduraru cb610cfdef 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>
(cherry picked from commit eb7324e7fc)
2026-07-25 23:15:22 +10:00
Petru Conduraru 345eb18124 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>
(cherry picked from commit 061bb90d50)
2026-07-25 23:15:22 +10:00
Petru Conduraru d47d71e734 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>
(cherry picked from commit a8d0ef3437)
2026-07-25 23:15:22 +10:00
Petru Conduraru 3f5f93a094 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>
(cherry picked from commit 438c0955f2)
2026-07-25 23:15:22 +10:00
Stephen Boddy d2e7d240a0 Git ignores CLAUDE.local.md file
This allows a file that will be automatically picked up by Claude. It can either
be a copy of a CLAUDE.md, or a one line file pointing to a shared common file. i.e.

@~/.claude/conventions-ifcopenshell.md

(cherry picked from commit b9deb9c63d)
2026-07-25 23:15:22 +10:00
Stephen Boddy c2133e324c 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>
(cherry picked from commit c0d2c2ea24)
2026-07-25 23:15:22 +10:00
Stephen Boddy 9100b327bf 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>
(cherry picked from commit 0ce6e94352)
2026-07-25 23:15:22 +10:00
Stephen Boddy 9237fe0072 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>
(cherry picked from commit be55400ec6)
2026-07-25 23:15:22 +10:00
Stephen Boddy 8b5933b67b 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
(cherry picked from commit 6f1737bb58)
2026-07-25 23:15:22 +10:00
Stephen Boddy 1132c3c385 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.

(cherry picked from commit c4605f2a8f)
2026-07-25 23:15:22 +10:00
Stephen Boddy 7036ea5c33 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.

(cherry picked from commit d5e890bccd)
2026-07-25 23:15:22 +10:00
Stephen Boddy 17177d5f1c 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.

(cherry picked from commit 9f848a73e1)
2026-07-25 23:15:22 +10:00
Stephen Boddy a941c664e1 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.

(cherry picked from commit 4fb8af2278)
2026-07-25 23:15:22 +10:00
Richard Brice a674dc5641 Fixes bug with fallback position introduced in 206cd6bb
(cherry picked from commit ade03b171a)
2026-07-25 23:15:22 +10:00
Richard Brice 9fc01de468 Stationing referent can optionally be located relative to the basis_curve (default) or the alignment curve
(cherry picked from commit b5c1b81ede)
2026-07-25 23:15:22 +10:00
Richard Brice 26c6281cc7 Locates positioning referent on the alignment curve, not the basis curve
(cherry picked from commit 47a20f0c7c)
2026-07-25 23:15:22 +10:00
Richard Brice 3cb614485a Fixes double unit conversion when convert-back-units are used
(cherry picked from commit 52d894298e)
2026-07-25 23:15:22 +10:00
Petru Conduraru 9621388953 ifcpatch: correct the AGS2IFC docstring example
The example block was copy pasted verbatim from ExtractPropertiesToSQLite,
so it named the wrong recipe and wrote a .sqlite file. These docstrings are
what ifcpatch surfaces as CLI and UI help, so anyone following the example
for AGS2IFC got a recipe name that does not match the one they selected.

Also state that the input file is not read and that a new IFC4X3 model is
built, since that is not obvious from the signature and the recipe creates
its own project rather than patching the one passed in.

Generated with the assistance of an AI coding tool.
2026-07-25 19:46:32 +10:00
Petru Conduraru 89523999b3 Bonsai: fix UnboundLocalError crash in polyline angle calculation
angle_round_threshold was only assigned inside the `distance > 0`
branch of calculate_distance_and_angle, but read unconditionally
whenever should_round is True. When the mouse sample coincides with
the last placed point (distance == 0), such as the first mouse move
after placing a wall's start point on a YZ plane view, this crashed
the modal wall tool.

angle_round_threshold is a fixed cutoff unrelated to whether distance
is currently zero, so it is now assigned once before the branch.

Fixes #8597.

Generated with the assistance of an AI coding tool.
2026-07-25 13:57:51 +10:00
Petru Conduraru 51ab38de27 Fix ci-bonsai-daily: configure unmerged_blobs mock in git_mergetool tests (#8574)
test_returns_none_when_report_file_absent/empty build a MagicMock repo
without configuring index.unmerged_blobs(), so it returned a truthy
MagicMock and git_mergetool's load-bearing "unresolved conflicts remain"
fallback (tool/ifcgit.py:646-647) returned that list instead of None -
failing "assert [] is None". The production fallback is correct and
intentionally left untouched; the tests just misrepresented the
"mergetool resolved cleanly" scenario they are named for. Set
mock_repo.index.unmerged_blobs.return_value = {} in both.

Verified in headless Blender: test/tool/test_ifcgit.py::TestGitMergetool
2 failed / 1 passed -> 3 passed.

This change was made with the assistance of an AI tool.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 23:21:18 +01:00
Petru Conduraru fbe36532a0 ifcdiff: fix crash when exporting property diffs to JSON
DeepDiff's dictionary_item_added/set_item_added results are a
deepdiff.helper.SetOrdered instance, which subclasses orderly_set's
StableSetEq rather than the OrderedSet class json_dump_default checked
for, so the property relationship check always crashed export() with
"Object of type SetOrdered is not JSON serializable". Check against
StableSet, the common base class shared by every orderly_set set
flavour, instead.

Fixes #8905

Generated with the assistance of an AI coding tool.
2026-07-25 07:47:13 +10:00
Andrej730 2f1b2f9638 ifcwrap: use swig shadowing for keeping reference to Element 2026-07-24 21:51:21 +05:00
Andrej730 9001cca078 ifcwrap: exclude internal geometry pointers
Still available as `int(xxx.this)`.
2026-07-24 21:51:21 +05:00
Andrej730 018695a2a9 file.ctor: use swig shadowing 2026-07-24 21:50:49 +05:00
Andrej730 2b7f55c1a0 stub: fixes after data model changes 2026-07-24 21:50:49 +05:00
Andrej730 9a62bf3c11 stub: updates after plugins were introduced 2026-07-24 21:50:49 +05:00
Andrej730 34f8a2c54e Serialized.setFile: use file ref instead of pointer for safety 2026-07-24 21:50:49 +05:00
Andrej730 310eaedc8e stub: group plugin search paths methods 2026-07-24 21:50:49 +05:00
Andrej730 3c381b0d00 stub: add updated get_info_cpp 2026-07-24 21:50:49 +05:00
Andrej730 4dd39ee918 downstream: stub: drop abstract_arrangement (158756e921)
And also gnore delete_same_facet_edge_pairs as it's more of an interanl API.
2026-07-24 21:50:49 +05:00
Andrej730 af32884731 downstream: stub: add logger_or_root arg type 2026-07-24 21:50:49 +05:00
Andrej730 a20c7484e5 downstream: stub: add missing arrange_polygon_settings (158756e921) 2026-07-24 21:50:49 +05:00
Andrej730 f7aa4504ec ifcwrap: ignore schema registry and plugins related structs and functions 2026-07-24 21:50:49 +05:00
Andrej730 c91d6d54bc new_IfcBaseClass: use ref for safety 2026-07-24 21:50:49 +05:00
Andrej730 189eeae719 register_schema: use ref to avoid segfaults
E.g. `register_schema(None)` from Python was resulting in a segfault
2026-07-24 21:50:49 +05:00
Andrej730 a954170927 downstream: ifcwrap: exclude interal geometry pointers
Still available as `int(xxx.this)`.
2026-07-24 21:50:49 +05:00
Andrej730 6fb5a619df downsteram: IfcSchema: provide arg names for register_schema, schema_by_name 2026-07-24 21:50:49 +05:00
Andrej730 849123acee downstream: ifcwrap: hide guess_file_type from Python as unused 2026-07-24 21:50:49 +05:00
Andrej730 9544641e41 downstream: stub: sync added/removed symbols 2026-07-24 21:50:49 +05:00
Andrej730 ec558dc57e stub: add logger_or_root arg type 2026-07-24 18:37:25 +05:00
Andrej730 2db5658386 ifcwrap: hide guess_file_type from Python as unused 2026-07-24 18:22:59 +05:00
Andrej730 88c8bd032f ifcwrap: fix breaking validate_stub (824c1fc)
It's ignoring underscore prefixed functions as not actually used.
Removing underscore to keep it happy without adding new exceptions.
2026-07-24 18:00:54 +05:00
Andrej730 3d8654acfd ifcwrap: ignore newly added conversion settings structs (183e4c4) 2026-07-24 17:59:44 +05:00
Andrej730 4a20b67038 IfcSchema: provide arg names for register_schema, schema_by_name 2026-07-24 16:48:51 +05:00
Andrej730 b14df627d7 ci: fix failing test for ifc5d 2026-07-24 16:15:54 +05:00
dependabot[bot] 0828c6ba92 build(deps): bump ty from 0.0.61 to 0.0.63
Bumps [ty](https://github.com/astral-sh/ty) from 0.0.61 to 0.0.63.
- [Release notes](https://github.com/astral-sh/ty/releases)
- [Changelog](https://github.com/astral-sh/ty/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ty/compare/0.0.61...0.0.63)

---
updated-dependencies:
- dependency-name: ty
  dependency-version: 0.0.63
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:46:39 +05:00
Andrej730 a586c7f695 black . 2026-07-24 15:43:59 +05:00
Andrej730 0bad5a9389 ty: add ignores 2026-07-24 15:43:59 +05:00
dependabot[bot] 0ce400cace build(deps): bump gersemi from 0.26.1 to 0.28.0
Bumps [gersemi](https://github.com/BlankSpruce/gersemi) from 0.26.1 to 0.28.0.
- [Release notes](https://github.com/BlankSpruce/gersemi/releases)
- [Changelog](https://github.com/BlankSpruce/gersemi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BlankSpruce/gersemi/compare/0.26.1...0.28.0)

---
updated-dependencies:
- dependency-name: gersemi
  dependency-version: 0.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:29:41 +05:00
dependabot[bot] 91ed59311b build(deps): bump ruff from 0.15.22 to 0.16.0
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.22 to 0.16.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.22...0.16.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:29:22 +05:00
dependabot[bot] 1906481a01 Bump svelte from 5.53.6 to 5.55.8 in /src/ifctester/webapp
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.53.6 to 5.55.8.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.55.8/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.55.8
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:06:42 +05:00
dependabot[bot] 73bf238232 Bump uuid and hyperid in /src/ifctester/webapp
Removes [uuid](https://github.com/uuidjs/uuid). It's no longer used after updating ancestor dependency [hyperid](https://github.com/mcollina/hyperid). These dependencies need to be updated together.


Removes `uuid`

Updates `hyperid` from 3.3.0 to 4.0.0
- [Release notes](https://github.com/mcollina/hyperid/releases)
- [Commits](https://github.com/mcollina/hyperid/compare/v3.3.0...v4.0.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 
  dependency-type: indirect
- dependency-name: hyperid
  dependency-version: 4.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:06:21 +05:00
dependabot[bot] a85d5cc990 Bump lxml from 4.9.1 to 6.1.0 in /src/ifcopenshell-python
Bumps [lxml](https://github.com/lxml/lxml) from 4.9.1 to 6.1.0.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.9.1...lxml-6.1.0)

---
updated-dependencies:
- dependency-name: lxml
  dependency-version: 6.1.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:04:15 +05:00
dependabot[bot] c16aec2cb0 Bump ws and engine.io-client in /src/ifctester/webapp
Bumps [ws](https://github.com/websockets/ws) and [engine.io-client](https://github.com/socketio/socket.io). These dependencies needed to be updated together.

Updates `ws` from 8.17.1 to 8.21.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.17.1...8.21.0)

Updates `engine.io-client` from 6.6.3 to 6.6.6
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/engine.io-client@6.6.3...engine.io-client@6.6.6)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.21.0
  dependency-type: indirect
- dependency-name: engine.io-client
  dependency-version: 6.6.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:03:56 +05:00
dependabot[bot] 76be31561e build(deps-dev): bump immutable in /src/ifctester/webapp
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 5.1.5 to 5.1.9.
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v5.1.5...v5.1.9)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 5.1.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:01:00 +05:00
dependabot[bot] a111c68d44 Bump devalue from 5.6.4 to 5.8.1 in /src/ifctester/webapp
Bumps [devalue](https://github.com/sveltejs/devalue) from 5.6.4 to 5.8.1.
- [Release notes](https://github.com/sveltejs/devalue/releases)
- [Changelog](https://github.com/sveltejs/devalue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/devalue/compare/v5.6.4...v5.8.1)

---
updated-dependencies:
- dependency-name: devalue
  dependency-version: 5.8.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 14:59:19 +05:00
dependabot[bot] 62f627ecc6 Bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 14:59:09 +05:00
dependabot[bot] 279e16f2ab build(deps-dev): bump tar from 7.5.16 to 7.5.21 in /src/ifctester/webapp
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.16 to 7.5.21.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.16...v7.5.21)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.21
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 14:58:54 +05:00
dependabot[bot] 3d68d0f0e5 build(deps-dev): bump postcss in /src/ifctester/webapp
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.4 to 8.5.22.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.4...8.5.22)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.22
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 14:58:50 +05:00
Petru Conduraru e759135608 Bonsai: cache-bust webui static assets so shipped JS/CSS changes reach users
Browsers were caching /static/js and /static/css for the standalone
webui (costing, gantt, drawings, index, demo pages) indefinitely, so a
shipped JS fix (e.g. the Download CSV button) would only reach a user
after a manual hard refresh.

Two changes, applied consistently across all five webui pages.

1. Every locally served link/script tag in the pystache templates now
carries a ?v=<bonsai version> query string, falling back to a static
asset mtime hash when BONSAI_VERSION isn't set (e.g. running
sioserver.py standalone). Since get_bonsai_version() includes the
build's commit hash, the token changes on every shipped update.

2. Responses under /static/ and /jsgantt/ now carry
Cache-Control: no-cache, must-revalidate. This covers what query
stamping alone can't reach: cost.js and gantt.js statically import
utilities/costui.js by a fixed relative path with no query string, so
that nested module still needed server side revalidation to pick up
changes.

Verified against a live aiohttp instance of sioserver.py: rendered
HTML for all five routes shows the stamped URLs, and the token
changes when BONSAI_VERSION changes between two server runs. A
conditional GET against a static file with a stale If-Modified-Since
header confirms the cheap 304 revalidation path still works.

Also used this instance plus a real headless Chromium (Playwright) to
click test the previously untested Download CSV button on the costing
page. The ribbon renders it correctly, and clicking it (with a
synthetic cost-items table injected into the DOM to stand in for a
connected Blender's data) triggers a real Blob download with the
correct filename and CSV content. No bug found, the button works as
intended.

AI-generated with Claude Code.
2026-07-24 11:32:34 +02:00
Petru Conduraru 1df738d968 ifc5d: match cost schedule export columns to the Bonsai cost panel (#6251)
Stefano's final ask on #6251 was specific: the ODS/XLSX export should
show exactly what the cost panel shows, ID (Identification), Name,
Quantity, Value, Total Cost, no more, no less. The previous fix in
this PR removed the internal bookkeeping columns but still exported
Description, Unit and a per-category cost breakdown (Labor Cost,
Material Cost, etc), none of which appear in the panel.

Presentation formats (.ods/.xlsx) now use an explicit allow-list of
columns instead of a block-list of internal ones, and relabel headers
to match the panel's own wording (ID / Value / Total Cost). The .csv
format is unchanged: csv2ifc still reads back the extra bookkeeping
columns for the import round trip, which is why it keeps them.

Also add a "Download CSV" button to the browser costing view
(Generate spreadsheet browser), which previously only offered a
clipboard-based Copy Selected. It reuses the already-rendered table
(respecting the user's column visibility settings) and triggers a
real file download, dropping only the UI-only Actions column.

AI-generated with Claude Code; reviewed and tested by Petru Conduraru.
2026-07-24 11:32:34 +02:00
Petru Conduraru 98c28a1f30 ifc5d: professional grade ODS/XLSX cost schedule export #6251
Three defects reported against the Costing tab export:

1. XLSX export crashed with ModuleNotFoundError: xlsxwriter was never
   bundled with Bonsai. Port the writer to openpyxl, which ifccsv
   already uses and Bonsai already ships, so it works out of the box.
2. Every ODS cell was written as a string (numbers as text), and the
   formula branch was dead code: it compared against 'Total Price' /
   'Rate Subtotal' while the headers are 'TotalPrice' / 'RateSubtotal'.
   Numeric columns are now typed float cells and TotalPrice becomes a
   real formula: Quantity*RateSubtotal on leaf items, SUM over the
   direct children's TotalPrice cells on sum items.
3. Internal bookkeeping columns (Id, ItemIsASum, Hierarchy, Index,
   Quantities) leaked into the presentation formats. ODS/XLSX now hide
   them; CSV keeps them since csv2ifc consumes them for the round trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 11:32:34 +02:00
Petru Conduraru 21122c0d28 Resolve nested complex quantity paths in the selector (#2041)
get_element_value could not reach the members of an IfcPhysicalComplexQuantity
(or IfcComplexProperty) by their natural path. util.element expands a complex
quantity into a dict whose nested members live under a "properties" sub-dict,
but the selector's dict navigation only looked at the top level, so
"Qto_Custom.Layer1.Width" returned None and IfcCsv exported nothing for it.
Only the internal "Qto_Custom.Layer1.properties.Width" path worked.

When a key is not a direct member of the value dict, descend into its
"properties" sub-dict so nested quantities/properties resolve with the
natural "Set.Complex.Nested" path. Direct keys still take priority, so the
explicit ".properties." path stays backward compatible and the regex branch
is untouched.

Verified: Qto_Custom.Layer1.Width -> 0.1 and Layer1.Height -> 2.5 (were
None), the sibling simple NetArea still resolves, the legacy .properties.
path still works, and IfcCsv now exports the nested value. test_selector.py:
38 passed (adds test_selecting_a_nested_complex_quantity).

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:31:38 +02:00
falken10vdl d63a99b70c Merge pull request #8342 from falken10vdl/style-flat-pretty-toggle
Style flat pretty toggle
2026-07-24 09:56:24 +02:00
falken10vdl f2d8f17f88 Remove unused has_any_textures return from restore_material_style_types 2026-07-24 09:47:16 +02:00
falken10vdl e9b619e3fb Remove unused _get_shader_label helper method 2026-07-24 09:41:23 +02:00
Petru Conduraru d1f9e5243e Fix ci-bonsai-daily: ProjectLibraryData duplicate parent-library enum entry (#8573)
* Fix ci-bonsai-daily: ProjectLibraryData duplicate parent-library enum

parent_libraries_enum() adds an explicit entry for get_root_context(),
then loops over cls.data["project_libraries"] (all IfcProjectLibrary
entities) and appends each. For a library-only file (no IfcProject),
get_root_context falls back to the top-level IfcProjectLibrary itself,
so the root is appended twice with the same enum key (its STEP id),
which Blender EnumProperty requires to be unique -> the data load
asserts. Normal project files are unaffected (root is an IfcProject
whose id never collides with a library id).

Skip library_id == root.id() in the loop (dedup by id, the colliding
key). Verified in headless Blender:
test_project_library_data.py::TestLibraryOnlyFile goes from 1 failed /
5 passed to 6 passed.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Bonsai: repair library files missing the required IfcProject, not just the symptom

Per the IFC Project Context concept template, every project data set (library
files included) shall contain exactly one IfcProject, and IfcProjectLibrary
instances are assigned to it via IfcRelDeclares. There is no such thing as a
spec-valid file rooted on IfcProjectLibrary alone.

get_root_context() (added in 260a387069, #8184) treated a missing IfcProject
as license to use the top-level IfcProjectLibrary as the file's root context
instead. That invalid premise is why project_libraries() (which walks every
IfcProjectLibrary, root included) then re-added that same entity, producing
the duplicate, colliding enum key this PR originally papered over with a
dedup guard.

Add tool.Project.ensure_project_context(), which repairs a file missing
IfcProject by creating one and declaring the file's root-level
IfcProjectLibrary instances to it, and tool.Project.open_library_file(),
which opens a library file through that repair. Route all three
IfcStore.library_file load sites in SelectLibraryFile through it. Downstream
code (get_root_context, ProjectLibraryData, RefreshLibrary,
AddProjectLibrary) now always operates on a spec-valid model, so the
duplicate enum entry cannot occur; the previous one-line dedup guard in
parent_libraries_enum() is kept only as cheap defense in depth for callers
that bypass the load-time repair, not as the fix.

Rework test_project_library_data.py: the previous _make_library_only_file()
fixture built an invalid library-only model and asserted that as correct
behaviour. Replace it with a spec-valid fixture (IfcProject + IfcProjectLibrary
declared to it) for the downstream tests, and a malformed fixture used only to
exercise the new repair path.

Verified live in headless Blender (isolated profile): reproduced the original
duplicate-enum-key failure mode, then confirmed ensure_project_context/
open_library_file repair a malformed file and ProjectLibraryData,
refresh_library and add_project_library all operate correctly on the result,
with no duplicate keys and no regression on already-valid files or IFC2X3.

This change was made with the assistance of an AI tool.

* Bonsai: stop supporting library-only files, do not repair them

Per Moult's feedback: if the IFC is invalid, our default position is to not
support it, not to patch around it. A library file with no IfcProject is
invalid IFC (Project Context concept template requires exactly one
IfcProject), and it is not ubiquitous: every library file bonsai ships under
bim/data/libraries has an IfcProject with the IfcProjectLibrary declared to
it via IfcRelDeclares. The single #8183 report is an outlier, not a common
authoring pattern worth accommodating.

Remove tool.Project.ensure_project_context() and open_library_file() (the
load-time repair added in the previous commit here) and revert
SelectLibraryFile's three load sites to plain ifcopenshell.open. Simplify
get_root_context() back to returning ifc_file.by_type("IfcProject")[0]
directly, no IfcProjectLibrary fallback: a file without IfcProject now raises
IndexError instead of being silently treated as valid. AddProjectLibrary's
nest-under-library branch is now dead code (root_context is always an
IfcProject) and is removed. The one-line enum dedup guard from the original
commit here is also removed: since get_root_context can only return an
IfcProject or raise, an IfcProject id can never collide with a library id, so
the guard has nothing left to guard against.

Rework test_project_library_data.py: drop the invalid _make_library_only_file
fixture and its tests, which asserted an unsupported model as correct
behaviour. Replace with a single spec-valid fixture matching bonsai's own
shipped library files (IfcProject + IfcProjectLibrary declared to it), used
for the ci-bonsai-daily regression test and the refresh/add-library
operators, plus one explicit test that get_root_context raises for a file
without IfcProject, documenting that this input is intentionally
unsupported rather than silently tolerated.

Verified live in headless Blender (isolated profile, source-loaded, never
the real profile): confirmed the removed methods are gone, that a
library-only file now raises instead of being handled, that
ProjectLibraryData/refresh_library/add_project_library all work correctly
on a spec-valid model with unique enum keys, and spot-checked that every
library file under bim/data/libraries already has an IfcProject.

This change was made with the assistance of an AI tool.

* Bonsai: inline get_root_context, trim docstrings, confirm get_parent_library unchanged

Per Moult's round 3 review. get_root_context added nothing over
ifc_file.by_type("IfcProject")[0], which is guaranteed by the IFC Project
Context concept template; remove it and inline the call at its three sites
(operator.py's RefreshLibrary and AddProjectLibrary, data.py's
parent_libraries_enum). Trim the get_parent_library docstring to one line;
its logic is untouched by this PR, byte for byte identical to origin/v0.8.0,
and still returns None only when project_library has neither Nests nor
HasContext, never for a library declared directly to IfcProject.

Rework test_project_library_data.py to match: replace the two
get_root_context-specific tests with one that exercises the real call site
(ProjectLibraryData.parent_libraries_enum raising IndexError for a file
without IfcProject), and add an explicit test that get_parent_library
returns None for a genuinely orphaned library. Also drop a long inline
comment that restated what the test body already shows.

Verified live in headless Blender (isolated profile, source-loaded, never
the real profile): all 17 test/bim/module/project tests pass, including the
new get_parent_library None-for-orphan case. Ran the full test/bim suite
before and after on the identical harness: 82 failed/1335 passed both times,
same failing tests (all pre-existing, unrelated to this module).

This change was made with the assistance of an AI tool.

* Bonsai: fix EditProjectLibrary leaving stale declarations after reparenting

Per Moult's round 4 review. The assertion change (get_parent_library(root)
now returns the IfcProject instead of None) is correct: in the old
library-only test model a top-level library had neither IfcRelNests nor
IfcRelDeclares, so None meant "top level". In the new spec-valid model a
top-level library is always declared to the guaranteed IfcProject via
IfcRelDeclares, so get_parent_library correctly resolves it through the
HasContext branch instead of falling through to None. get_project_hierarchy
already keys top-level libraries under the project for exactly this reason,
so the library tree still renders correctly.

Auditing every caller found one real bug in EditProjectLibrary, which
Gorgious56 originally wrote for the library-only model. Its move-library
logic assumed a top-level library (previous_parent_library is None) needed
no cleanup before nesting it under a new parent, and that unnesting a
library back to the project needed no new relationship because it was
"already assigned by default". Both assumptions relied on a top-level
library never actually holding a IfcRelDeclares, which is no longer true.
Reproduced live: moving a project-declared library under another library
left its old IfcRelDeclares dangling alongside the new IfcRelNests (an
invalid double parentage), and moving a nested library back to the project
left it with neither relationship, orphaning it out of the tree entirely.

Fixed by tearing down whichever of IfcRelDeclares/IfcRelNests the library
previously had before establishing whichever one the new parent requires,
instead of assuming which prior state applies.

Added tests: get_parent_library resolving a nested sub-library to its
library parent (the third contract case alongside project-declared and
orphaned), and both EditProjectLibrary reparenting directions, which fail
without the operator.py fix and pass with it.

Verified live in headless Blender (isolated profile, source-loaded, never
the real profile): all 20 test/bim/module/project tests pass. Ran the full
test/bim suite before and after on the identical harness: 123 failed/1294
passed before, 123 failed/1297 passed after, identical failing test names
in both runs (diffed), the extra 3 passes are the new tests above.

This change was made with the assistance of an AI tool.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:31:44 +10:00
Dion Moult 4972bb7a81 ifcviewer: reorder sidecar by Morton in the offline bake path
SidecarBuilder::build (the one-shot bake used by the models-panel export
command in bonsaiviewer) never called reorderSidecarByMorton, unlike the
live streaming loader. The chunk table was therefore left empty, so the
exported .ifcview had its geometry laid out non-contiguously and only the
metadata blocks compressed. Reorder before writeSidecar to match the
loader so exported sidecars stream correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:22:31 +10:00
Petru Conduraru 537317b26f Fix ci-bonsai-daily: guard on_depsgraph_update_caps during file load
on_depsgraph_update and on_depsgraph_update_caps are registered together
as persistent depsgraph handlers (bim/module/clip_box/__init__.py:50-52).
on_depsgraph_update guards with `if cls._file_loading: return`, but the
sibling on_depsgraph_update_caps did not, so a depsgraph tick during the
file-load window still ran it. Beyond the failing test, this can re-arm a
cap-rebuild bpy.app.timers callback in the exact load window _on_load_pre
cancels timers for, against regions whose GPU state is not yet wired.

Add the same _file_loading guard as the first check.

Verified in headless Blender:
test_clip_box.py::TestRefreshTimerLifecycle::test_depsgraph_update_no_op_while_loading
1 failed -> passed.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:22:01 +10:00
Petru Conduraru 4a095f9810 Bonsai: don't crash querying a freshly linked IFC with cache off
Link IFC with 'Use Cache' unchecked crashed with FileNotFoundError
when no .ifc.cache.blend existed yet (a fresh link). Regression from
35e3d9c42, which refactored the cache-clear guard from
'if not self.use_cache and blend_filepath.exists()' into
should_clear_cache() but dropped the existence check on the
not-use_cache path, so os.remove() ran on a non-existent file.

Check blend_filepath.exists() first in should_clear_cache() so the
remove is never attempted when there is nothing to clear, while
keeping the query-mismatch cache invalidation intact.

Fixes #8350

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:19:12 +10:00
Dion Moult 466df653b7 docs: add installation pages for bonsaiviewer and ifcviewer
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:15:09 +10:00
falken10vdl c92a825a94 Cache last shading type to skip redundant material style restores 2026-07-24 09:12:04 +02:00
falken10vdl 4c5bd88877 add material update_tag in restore_material_style_types 2026-07-24 09:12:04 +02:00
falken10vdl 92e3e400f8 Use consistent material style prop accessor 2026-07-24 09:12:04 +02:00
falken10vdl 13c4ba257d Fix initila style when loading (default is SOLID - Flat: Shade) 2026-07-24 09:12:04 +02:00
falken10vdl c77a28c862 Add Flat/Pretty style toggle and dual-branch external style management 2026-07-24 09:12:04 +02:00
Dion Moult ede689a8ff ifcviewer-web: fix two streaming stalls found in battle testing
driveStreamingLoads could deadlock: a chunk waiting on asynchronous
pool growth parks in a frame-counted backoff cooldown, but once the
render loop quiesced after the settle burst the frame index froze, so
the cooldown never expired and streaming stalled part-loaded until the
user moved the camera. Keep the loop alive while growth may still land
(growth_pending() || can_grow()), exposed via a new BufferPool accessor.

loadSidecarMetadataWeb put the model in the scene before reading the
element-metadata block header, leaving a window where the locator was
still zero. A getObjects() landing in that window could not distinguish
"locator not read yet" from "sidecar has no element block" and latched
the model as permanently empty. Read the 16-byte header first, then
apply; carry the locator through applyCachedModel so it is set before
any web element-metadata fetch can run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 16:59:15 +10:00
Petru Conduraru 82f73c29ea style.assign_representation_styles: fix crash on IfcPresentationStyleAssignment #7883
When replacing a style on an item whose previous IfcStyledItem wraps its styles
in the deprecated IfcPresentationStyleAssignment, and the assignment is not
being reused (use_style_assignment is False, e.g. an IFC4 file authored by
AVEVA E3D), the else branch called remove_same_type_styles(style_assignment)
with style_assignment still None, raising
AttributeError: 'NoneType' object has no attribute 'Styles'. Operate on style_,
the assignment found in the current iteration, instead of the accumulator.
Verified red-green with a minimal IFC4 file using IfcPresentationStyleAssignment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 16:46:29 +10:00
Petru Conduraru 209c44db83 Selector: negate list comparisons as an aggregate #8129
compare() recursed into list values passing the negated comparison through,
so != meant "at least one item differs" and both = and != matched the same
elements on any multi-valued property (e.g. an enumerated property with two
values selected). Strip the negation for the per-item comparison and negate
the aggregate instead, so != means "no item equals" and stays the complement
of =. The same applies to !*=.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 16:24:03 +10:00
Dion Moult e4f8475ce8 bonsaiviewer: View Selected Model from the models panel context menu
Right-clicking a model in the models panel now offers "View Selected Model",
which frames the camera on just that model's geometry — View All, scoped to
one model. With several models selected the action reads "View Selected
Models" and frames their union, matching how the panel's existing Move to
Group already treats a multi-selection.

The AABB fold behind viewAll moves into InstanceCompose, which exists so this
kind of logic is unit-testable without a Qt window or a wgpu device (populating
ViewportCore's model map needs a real GPU, so the fold was previously
untestable in place). It splits in two:

- sceneWorldAabb   — every VISIBLE model, what viewAll frames.
- modelsWorldAabb  — only the named models, hidden or not. A model the caller
                     named explicitly is framed even if hidden; second-guessing
                     that is worse than honouring it. Models with no loaded
                     geometry contribute nothing, and if none of them do the
                     camera is left alone rather than flying to the origin.

Both are covered by six new cases in test_instance_compose (131 total).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:42:44 +10:00
Dion Moult 89bb3074de ifcviewer-web: JavaScript scripting API (camera, selection, visibility, colour)
Give host pages a real API over the web viewer, not just "embed it and listen
for picks": read/set the camera, read/set multi-selection, enumerate every
object with its IFC identity, drive per-object visibility, and override
object colours.

The wasm boundary keeps to object_ids (u32 arrays marshalled through the heap,
with an "ask twice" convention on the getters); web/ifcviewer.js layers IFC
GlobalId resolution on top, from the element table getObjects() fetches. Every
id-taking call accepts an objectId, a GlobalId, or an element object.

Colour override needed no new mechanism: color_override_rgba8 was already
plumbed through the sidecar, the instance SSBO, the WGSL shader and the
opaque/transparent cull classifier, but nothing ever wrote a non-zero value
into it. setObjectsColor is the missing writer, which is why an alpha below 255
correctly reclassifies the instance into the transparent pass.

Two bugs surfaced while wiring this up:

- wgpu_initialized_ was only ever set by the Qt desktop host, so on web every
  upload guarded on it was a silent no-op — including the pre-existing
  recomposeAndUploadModel that federation transforms depend on. The core now
  latches it in its own web init.

- The demo pages were copied into the build dir by a POST_BUILD command on the
  wasm target, so they only refreshed when the wasm itself relinked; editing a
  page left a stale copy that the dev server (and the Playwright suite) kept
  serving. Each page now has its own copy rule with a real dependency, and
  sample.ifcview is a LINK_DEPENDS so regenerating it forces a relink.

applyCachedModel also now keeps the element metadata it already parses on the
path-based load (it was being dropped), so the embedded sample has GUIDs and
the demo works with no file to pick.

The sample model was three coincident cubes, which made per-object hide and
colour look like no-ops — whatever you hid was still drawn by the box behind
it. make_sample.py regenerates it as a slab, a wall and a beam in distinct
places, so the fixture is reproducible rather than an opaque blob.

Demoed by web/scripting.html (linked from the index; viewer is on
window.viewer) and covered by tests/scripting.spec.mjs — 6 cases against a real
GPU, asserting visibility and colour at the pixels, not just at the API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:42:44 +10:00
Petru Conduraru 382f5e0c21 ifcpatch: use stdlib graphlib for Optimise topological sort (#4399)
The Optimise recipe imported `toposort`, a third-party PyPI package that
is not bundled with Bonsai, so running the recipe there raised
`ModuleNotFoundError: No module named 'toposort'`.

Replace it with the standard library `graphlib.TopologicalSorter`
(available since Python 3.9), which provides the same dependencies-first
ordering guarantee the recipe relies on: forward-referenced instances are
mapped before the instances that reference them. The dependency-graph
dict format ({node: {predecessors}}) is identical between the two, so the
graph construction is unchanged. Drop `toposort` from ifcpatch's
dependencies since it is no longer used.

Verified with toposort NOT installed: the Optimise recipe now runs and
deduplicates correctly (IfcParseExamples_test.ifc 88 -> 63 instances, all
6 products preserved, output reopens cleanly).

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 14:54:49 +10:00
Petru Conduraru 81a42cec1a Bonsai tests: give bSDDClientStub the client baseurl attribute
tool.Bsdd.identifier_url() (pset/ui.py pset name check in the Property
Sets panel) reads client.baseurl unconditionally, but the test stub
never had that attribute, so any scenario that opens the Property Sets
panel dies with AttributeError under the stub. The boolean.feature
scenarios only surfaced this once their STEP id failures were fixed,
the id failure had been masking it. Mirror the real bsdd.Client
default so identifier_url() resolves to the standard identifier URL.

This change was made with the assistance of an AI tool.
2026-07-24 14:45:18 +10:00
Petru Conduraru 45fa04a94b Bonsai tests: stop hardcoding STEP ids in boolean.feature
The two boolean.feature scenarios pinned representation item objects by
absolute STEP id (Item/IfcHalfSpaceSolid/90, the BBIM_Boolean pset text
[91]). Those ids shift every time any earlier entity allocation in an
empty project changes (latest instance: #8577 moved 90 to 86), so this
cluster re-breaks on unrelated commits.

Make the object-name and panel-text BDD steps run their argument through
replace_variables, the same substitution 'the variable' and the
connection steps already use, and have boolean.feature capture the real
ids from the IFC file (by_type(...)[0].id()) into variables at the point
the entities are created. The steps stay strict: the substituted name
must still resolve to exactly the named object, there is no wildcard
matching. Substitution is a no-op for every existing feature string
without a {variable} placeholder.

This change was made with the assistance of an AI tool.
2026-07-24 14:45:18 +10:00
Petru Conduraru e27624c77f Fix ci-bonsai-daily BDD: OperatorSpy.bl_rna + stale MEP port name
Two independent test-harness/fixture defects in test/bim/test_feature.py:

- OperatorSpy had no bl_rna, so any BDD step that redraws a panel calling
  helper.draw_filter() (which tests "module" in op.bl_rna.properties)
  crashed with AttributeError. Give OperatorSpy a bl_rna property that
  forwards to the real registered operator class
  (bpy.types[bl_idname].bl_rna), matching live UILayout.operator()
  semantics. Fixes test_select_all_walls and test_edit_filter_query.
- The shared "I create default MEP types" step looked up
  bpy.data.objects["IfcDistributionPort/Port"], but port creation never
  sets port.Name, so tool.Loader.get_name deterministically names the
  object "IfcDistributionPort/Unnamed". Update the literal. Fixes the MEP
  scenarios (connect/transition/bend) that share this setup.

Verified in headless Blender: OperatorSpy scenarios 2 passed (were
AttributeError); MEP test_connect_mep_elements* go from
KeyError 'IfcDistributionPort/Port' to passing.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:39:53 +10:00
Ryan Schultz 836d57e7ff Fix #7774: Fix Select Similar failing on pset names with spaces
Pset names containing spaces (e.g. "SOLIDWORKS Custom Properties") were
not quoted when building selector keys in SelectSimilarData, causing
get_element_value to fail when the operator ran. Now wraps pset names
and property names in double quotes if they contain spaces, consistent
with the selector syntax used elsewhere.

Generated with the assistance of an AI coding tool.
2026-07-24 14:30:29 +10:00
Petru Conduraru 3d7d1ff4f3 ci: drop the ColumnPSetsOfSets.ifc fixture change, conflicts upstream
Per aothms's review comment: this file's schema was already changed
independently on v0.8.0 since this branch was created, so this PR's own
edit conflicts with it. Reverting to the current upstream version of the
fixture; the bsdd.py rate-limiting fix is untouched.
2026-07-24 14:28:18 +10:00
Petru Conduraru 6911418c67 ci: fix bSDD 429 rate limiting and restore ColumnPSetsOfSets.ifc schema
bsdd.py: the Client made every request with a bare requests.get, so a single
429 from the (unauthenticated, aggressively rate limited) bSDD API failed the
whole test. Route requests through a Session with a mounted urllib3 Retry
(5 attempts, backoff, honouring Retry-After) for 429/5xx, matching how a
resilient API client should behave, not just papering over the test.

ColumnPSetsOfSets.ifc: FILE_SCHEMA was accidentally changed from IFC4X3_ADD2
to IFC2X3 in a7738eeb64 (an unrelated logger refactor), a one line collateral
edit to this fixture. The file's DATA section still uses IFCPROPERTYSETDEFINITIONSET,
an IFC4+ only type. Parsing it against IFC2X3 threw "Entity ... not found in
schema", which silently fell back to interpreting the value as a raw nested
aggregate instead of the intended defined-type wrapper, producing the
double-nested tuple that broke test_stream, test_file and test_rocks in
test_streaming_rocksdb_and_simpletyperefs.py. Restoring the original schema
declared when the fixture was added (ff3fa48332) fixes all three.

Generated with the assistance of an AI coding tool.
2026-07-24 14:28:18 +10:00
Petru Conduraru 5c8eab981c Fix ci-bonsai-daily: get_dictionaries no longer clobbers injected client
Bsdd.get_dictionaries() unconditionally did cls.client = bsdd.Client(),
replacing whatever client was already set - including the
bSDDClientStub the BDD suite injects at module load
(test_feature.py: tool.Bsdd.client = bSDDClientStub()) to avoid live
network calls. Because "Load bSDD Dictionaries" is the first step of
every bsdd.feature scenario, the stub was discarded before its fixture
data ("LCA", "BonsaiTestDict") could ever be returned.

The re-init is unnecessary: bsdd.Client.__init__ only sets baseurl and
blank tokens, and the next line already updates baseurl defensively via
hasattr. Drop the clobbering assignment; reuse whichever client is
already set.

Verified in headless Blender: bsdd scenarios (load dictionaries, search
all/single dictionary) go from 3 failed ("Could not see LCA/
BonsaiTestDict") to 3 passed.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:24:34 +10:00
Petru Conduraru a155a1ca80 Fix ci-bonsai-choco: choco_release.py still targets the old choco/blenderbim path
The choco dir was renamed from choco/blenderbim to choco/bonsai back in
2024 (Rename choco dir), but choco_release.py's BLENDERBIM_DIR constant
was never updated, so the daily choco release job crashes immediately
with FileNotFoundError trying to os.chdir into the now nonexistent
choco/blenderbim directory.

Release tags also moved from a bare blenderbim-YYMMDD scheme to
bonsai-X.Y.Z-alphaYYMMDDHHMM, so the tag-prefix strip used to build the
package version still looked for the old "blenderbim-" prefix and left
it untouched, embedding the raw tag (including the already-present
"-alpha" segment) into the nuspec version field, which the template
then doubled up with its own "-alpha" suffix, producing an invalid
NuGet version string. Both are fixed together since the second bug
would otherwise surface as soon as the first one is unblocked.

The pre-commit black hook also reformatted pre-existing whitespace
drift in choco_release.py (this file sits outside CI's lint scope, so
it had never been auto-formatted before); that reformatting is
incidental to satisfying the local hook, not part of the fix itself.

Generated with the assistance of an AI coding tool.
2026-07-24 14:23:00 +10:00
Petru Conduraru 8c434b7167 ifcopenshell.util.element: dedupe SET-typed attributes in replace_attribute
replace_attribute() rewrites references inside aggregate attributes via
element.walk(), but never checked whether the replacement value was
already present elsewhere in the same aggregate. For an EXPRESS SET
(e.g. IfcProject.RepresentationContexts, IfcRelAggregates.RelatedObjects)
this can leave the same reference listed twice, which is invalid IFC.
LIST and BAG aggregates legitimately allow duplicates, so a blanket dedup
would be wrong; only SET-typed attributes are deduplicated, determined at
runtime from the schema declaration (IfcOpenShell#8706 review comment).

The SET/LIST/BAG check is cached per (schema, class, attribute index), and
the dedup pass itself only runs when a cheap linear pre-check finds the
replacement value already present in the aggregate, so the common case
(no duplicate produced) pays only that pre-check, not a hash-set rebuild.
Benchmarked against a 23MB (431k entities) and a 104MB (2.4M entities) IFC
model against a large SET attribute: worst case adds well under 1ms per
call; the realistic case (merging duplicate contexts, matching the PR
#8706 scenario) shows no measurable regression.

Fixes the root cause flagged in IfcOpenShell#8706 (Moult), obviating the
need for MergeDuplicateContexts' own manual aggregate-dedup pass for that
scenario.

Generated with the assistance of an AI coding tool.
2026-07-24 14:21:31 +10:00
Petru Conduraru 8fb8966094 docs: cover reading properties and quantities from an element and its type in C++ getting started
Fixes issue #3910's documentation gap. IsDefinedBy() returns
IfcRelDefinesByProperties relationship objects, not the property set
itself, and RelatingPropertyDefinition() must be used to reach the
IfcPropertySet or IfcElementQuantity. Properties can also come from an
element's type via IsTypedBy() -> RelatingType() -> HasPropertySets(),
a path that is easy to miss because it works differently. Adds a
worked, beginner-commented, compilable example covering both paths.

Generated with the assistance of an AI coding tool.
2026-07-24 06:14:34 +02:00
Petru Conduraru d506f5df1e Bonsai: compute earthworks base quantities in ifc5d take-off #6325
The ifcopenshell take-off engine left every Qto_EarthworksFillBaseQuantities
value null, so Bonsai added the qset with no numbers on IFC4X3 models. Map the
geometrically derivable quantities using the slab axis convention: Length on
local X, Width on local Y, Depth on local Z, and the net solid volume as the
compacted (Fill) or undisturbed (Cut) volume. LooseVolume and Weight stay
unmapped because they need soil bulking and density factors absent from
geometry. Bring IfcEarthworksCut to parity with the Blender engine and wire
IfcReinforcedSoil on both engines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 20:02:57 +02:00
Petru Conduraru e23142d01a Fix #7331: derive cost item quantities from IfcSpace base quantities
assign_cost_item_quantity skipped every IfcSpatialElement, which also
swallowed IfcSpace. Spaces are legitimate quantifiable objects, so their
Qto_SpaceBaseQuantities (for example GrossFloorArea) were never picked up
and count based cost items fell back to 0. Keep skipping spatial
containers (site, building, storey) but allow IfcSpace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:58:44 +02:00
Petru Conduraru 6085894433 Fix #8570: populate ifc5d IfcOpenShell QTO formulas for IfcSpace
The headless "IfcOpenShell" calculator had all Qto_SpaceBaseQuantities
formulas set to null for IfcSpace in both IFC4QtoBaseQuantities.json and
IFC4X3QtoBaseQuantities.json, so qto.py's `if not formula: continue`
skipped every quantity, no geometry task was queued, and spaces never
appeared in results (elements_quantified: 0). The Blender calculator
already computes these; they were just never ported to the
ifcopenshell.util.shape-backed calculator.

Map the eight computable quantities to existing util.shape functions,
mirroring the Blender calculator semantics (no new shape.py code):
GrossFloorArea=gross_get_footprint_area, NetFloorArea=net_get_footprint_area,
GrossCeilingArea=gross_get_top_area, NetCeilingArea=net_get_top_area,
GrossPerimeter=gross_get_footprint_perimeter, GrossVolume=gross_get_volume,
NetVolume=net_get_volume, Height=net_get_z.

Left null (matching the Blender ruleset, not guessed): GrossWallArea,
NetWallArea, NetPerimeter (Blender stub), and FinishFloor/CeilingHeight
(Blender derives these from sibling IfcCovering decomposition geometry,
which this per-element calculator architecture can't reach).

Verified on IFC4 (4x3 space extruded 2.5m): before -> {} / elements_quantified 0;
after -> GrossFloorArea 12, GrossPerimeter 14, Height 2.5, GrossVolume 30,
etc. - all exact matches to the extrusion. IFC4X3 formulas are identical
and the formula->function resolution is schema-agnostic.

Scope: fixes the IfcSpace case (the issue title). The 12 other all-null
classes noted in the issue (IfcDoor, IfcSite, IfcRailing, ...) are left as
follow-up.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 19:55:02 +02:00
Petru Conduraru c61ebe3876 Fix IfcCovering Qto_CoveringBaseQuantities mismatch between calculators (#6728)
The ifc5d IfcOpenShell (geometry-based) Qto engine computed
Qto_CoveringBaseQuantities using axis-agnostic heuristics:

- GrossArea/NetArea: gross_get_max_side_area / net_get_max_side_area,
  the largest of the X/Y/Z projected side areas.
- Width: gross_get_min_xyz, the smallest of the X/Y/Z dimensions.

The Blender Qto engine instead already used
EPset_Parametric.LayerSetDirection (AXIS2 for wall-like coverings,
AXIS3 for floor/ceiling-like coverings) to pick the correct axis via
get_covering_gross_area/get_covering_net_area/get_covering_width in
bonsai/bim/module/qto/calculator.py.

For any covering whose length isn't the largest dimension (e.g. a
short wall-covering strip, or a small covering patch), the two
engines' heuristics can pick different faces/axes entirely, giving
different Width/Area values for the same element - this is what was
reported in #6728.

Fix: give the IfcOpenShell engine the same layer-set-direction
awareness. Added IfcOpenShell.get_covering_parametric_axis/
get_covering_area/get_covering_width (dispatched as internal
functions, like the existing get_weight/get_segment_length), and
wired gross_get_covering_area/net_get_covering_area/
gross_get_covering_width into the IfcCovering rules in
IFC4QtoBaseQuantities.json and IFC4X3QtoBaseQuantities.json.

The AXIS2 area/width formulas (get_side_area, net_get_y) intentionally
match the simpler formulas already used for Qto_WallBaseQuantities in
this same rule set (net_get_side_area/net_get_y), rather than
replicating the Blender engine's more elaborate get_lateral_area/
get_width (min(X,Y)) helpers, consistent with how the two engines
already diverge for regular walls without being considered a bug.

Verified with a standalone script driving ifc5d.qto.IfcOpenShell
directly against synthetic AXIS2/AXIS3 IfcCovering geometry: for
typical proportions old and new formulas agree, and for
disproportionate coverings (thin dimension not the smallest/largest)
the old formulas picked the wrong axis while the new ones correctly
track the covering's LayerSetDirection, matching the Blender engine.
Did not verify through the full Blender/Bonsai UI, as it would have
required registering the addon in the machine's shared Blender
profile, which is unsafe while other agents may have it loaded.

Generated with the assistance of an AI coding tool.
2026-07-23 19:51:03 +02:00
carlopav 305f8c6003 cost: don't leave copied cost items in the copied schedule (#8851)
copy_cost_item appends the copy to the inverse relationships of the
original cost item, which for a root cost item includes the source
schedule's IfcRelAssignsToControl. copy_cost_schedule then assigned that
same cost item to the new schedule as well, so the copies showed up in
both schedules and deleting them from one removed them from the other.

Unassign the copy from the source schedule before assigning it to the
new one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:04:38 +02:00
Petru Conduraru 98a25eca96 Bonsai: wire Shift+Q quantity take off hotkey into Spatial tool
Fixes #4443. The Wall/Slab/other authoring tools (BimTool subclasses)
already bind Shift+Q to bim.perform_quantity_take_off via hotkey_S_Q,
but the Spatial tool has its own separate keymap/operator
(bim.spatial_hotkey) that never registered a Q entry, forcing users to
switch tools just to (re)calculate quantities for a selected element.
Added the same Shift+Q keymap entry and a matching hotkey_S_Q handler
to the Spatial tool, mirroring BimTool's existing behavior exactly
(including the same selected-objects guard).

The other part of the request, a bulk "calculate all quantities"
entry point, already exists today: bim.perform_quantity_take_off
computes quantities for every IfcElement when no objects are
selected, exposed via the Scene > Quantity Take-off panel regardless
of which workspace tool is active, so no change was needed there.

AI-generated, reviewed and tested by BIMvoice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 07:19:04 +02:00
Petru Conduraru 99c370b755 Bonsai: allow nesting element type objects together (#2283)
can_nest() only permitted IfcElement-to-IfcElement pairs, so nesting
two IfcElementType objects (e.g. an IfcElementAssemblyType nesting a
component IfcDoorType) was silently rejected. IfcRelNests.RelatingObject/
RelatedObjects are typed as the general IfcObjectDefinition in the
schema, so type-to-type nesting is schema legal, IfcOpenShell's core
nest.assign_object API already handles it generically, and the Nest
UI panel is driven purely by ifcopenshell.util.element.get_nest/
get_components (IFC data queries, not Blender collection structure),
so once the relationship exists it displays correctly with no other
changes needed.

Extended is_compatible_class to also accept a same-kind IfcTypeProduct
pair. Mixing an occurrence element with a type is intentionally still
rejected, that isn't a real modeling pattern.

Verified live in headless Blender: type-to-type nesting now creates
a real IfcRelNests and the Nest panel's own data functions reflect
it correctly; mixing an occurrence with a type is still rejected;
existing element-to-element nesting is unaffected.

Generated with the assistance of an AI coding tool.
2026-07-23 07:19:04 +02:00
Petru Conduraru f3cb7aea61 ifc5d: wire up GrossFootprintArea/NetFootprintArea for IfcWall QTO
Root cause: the IfcOpenShell-geometry-engine calculator ruleset
(IFC4QtoBaseQuantities.json and IFC4X3QtoBaseQuantities.json) left
IfcWall's GrossFootprintArea/NetFootprintArea mapped to null, so
these two quantities were silently omitted from Qto_WallBaseQuantities
whenever that ruleset was used. The generic gross_get_footprint_area
and net_get_footprint_area formulas already exist and are already
wired up for IfcSlab in the same files, so this was a missing mapping,
not a missing implementation.

Fixes #7029.

Generated with the assistance of an AI coding tool.
2026-07-23 07:16:01 +02:00
Andrej730 4cd9d4b53a drawing: update use of settings after introduction of plugins 2026-07-22 19:01:51 +05:00
Andrej730 3536bcf46d entity_instance: switch to use of declaration as a property
Fixes most of the bonsai tests.
2026-07-22 19:01:51 +05:00
Andrej730 c2badadfc4 cmake: skip compiled extensions when installing ifcwrap sources 2026-07-22 19:01:51 +05:00
Andrej730 c138d68ad1 dev-setup: use Python 3.13 2026-07-22 19:01:51 +05:00
Andrej730 037908f08e dev_environment.py: add --skip-binaries flag 2026-07-22 19:01:50 +05:00
Andrej730 1e4b4b5557 test_getting_elements_by_profile: fix test 2026-07-22 19:01:50 +05:00
Andrej730 5786d1e4bd test_global_id_updated: fix expected message (a07f56db6) 2026-07-22 19:01:50 +05:00
Andrej730 1bc5134578 ifcopenshell: fix stale reference to _file 2026-07-22 19:01:50 +05:00
Andrej730 298e4cfd8d instance_streamer: migrate to snake case 2026-07-22 19:01:50 +05:00
Andrej730 e98a3a5c2c test_create_shape: fix use of renamed OutputFormat 2026-07-22 19:01:50 +05:00
Andrej730 ab38f4f7f6 Consider new geometry settings 2026-07-22 19:01:50 +05:00
Andrej730 01cfbe6948 Consider new serializer settings 2026-07-22 19:01:50 +05:00
Andrej730 872efac4d2 parse_ifcxml: drop the use of the removed method 2026-07-22 19:01:50 +05:00
Andrej730 32f2dfd294 downstream: logger: reuse logger_or_root, dedupe optional-logger-arg pattern 2026-07-22 19:01:50 +05:00
Andrej730 d45174066f downstream: logger: use Logger* instead of Logger& to propagate signature using swig 2026-07-22 19:01:50 +05:00
Andrej730 e50bace056 stub: drop serializer classes
Superseded by generic `create_geometry_serializer`
2026-07-22 19:01:50 +05:00
Andrej730 5f0f4669f2 Fix using logger.Root instead of logger.root 2026-07-22 19:01:50 +05:00
Andrej730 c4e411c735 Drop use of removed entity_instance.wrapped_data 2026-07-22 19:01:50 +05:00
Andrej730 9286bcd7ec .gitignore: ignore ifcopenshell plugins 2026-07-22 19:01:50 +05:00
Andrej730 f733502757 Replace removed get_info_2 2026-07-22 19:01:50 +05:00
Andrej730 6ff35f4a00 IfcParseWrapper: use swig feature to override base classes
Needed to make all attributes be resolvable statically.
2026-07-22 19:01:50 +05:00
Andrej730 e6a7f7513d entity_instance: comparison operators to support non-entity types 2026-07-22 19:01:50 +05:00
Andrej730 6f643bad0c entity_instance: fix using get_info before it's defined 2026-07-22 19:01:50 +05:00
Andrej730 4275b23a27 ifcviewer: fatal_error on missing patchelf instead of warning 2026-07-22 19:01:50 +05:00
Andrej730 9d6e6ddf60 build-all: add error msg on missing art module 2026-07-22 19:01:50 +05:00
Andrej730 78e518c55d build_rocky: drop unused DARWIN_C_SOURCE variable 2026-07-22 19:01:50 +05:00
Andrej730 2e9ded3f15 build_rocky: split build command for readibility 2026-07-22 19:01:50 +05:00
Andrej730 c5235cbc99 build_rocky: drop unused typing_extensions 2026-07-22 19:01:49 +05:00
Andrej730 3efb2c2d5f Fix ruff complaints 2026-07-22 19:01:49 +05:00
Andrej730 d7f9ade853 build-all: fix issue building rocksdb on gcc 15
Example error: `error: ‘uint64_t’ has not been declared uint64_t blob_file_number, uint64_t total_blob_count,`

See https://github.com/facebook/rocksdb/issues/13365
2026-07-22 19:01:49 +05:00
Andrej730 d4f0e66f08 build-all: fix issue on gcc 15
Error was:
```
configure: error: could not find a working compiler, see config.log for details
```

config.log:
```
conftest.c: In function 'f':
conftest.c:12:48: error: too many arguments to function 'g'; expected 0, have 6
   12 | for(i=0;i<1;i++){if(e(got,got,9,d[i].n)==0)h();g(i,d[i].src,d[i].n,got,d[i].want,9);if(d[i].n)h();}}
      |                                                ^ ~
```
2026-07-22 19:01:49 +05:00
Andrej730 2e11208836 build-all: note on BUILD_BONSAIVIEWER 2026-07-22 19:01:49 +05:00
Andrej730 ba3801718f downstream: surveyor: drop never used dead code
Surveyor test was failing because `get_z_rotation` and `set_z_rotation` were not implemented.
The code was added in 230cbe1fd8, but it was never used.
2026-07-22 19:01:49 +05:00
Andrej730 147119d5f6 downstream: bsdd: raise informative HTTPError
Previously we were just passing `.json()` which allowed too many request
error slip in to later occur as missing attributes on the dictionaries.
2026-07-22 18:51:53 +05:00
Andrej730 ecbd8941de downstream: bsdd: warn about include_class_properties deprecation
See https://github.com/buildingSMART/bSDD/issues/149
2026-07-22 18:51:53 +05:00
Andrej730 9296fd8d1f downstream: misc: add Blender 5.2 offset for Quick Favorites user_menus 2026-07-22 18:49:38 +05:00
Andrej730 84b2cf6db0 dev-setup: use Python 3.13 2026-07-22 18:47:44 +05:00
Andrej730 dc1d35ce8b downstream: bonsai tests: fix test_failed_to_load_returns_only_base_keys (fdb2947) 2026-07-22 18:41:30 +05:00
Andrej730 113643c916 dev_environment.py: add --skip-binaries flag 2026-07-22 18:36:24 +05:00
Andrej730 16c1d2ece3 downstream: core.drawing: deduplicate code, fix test
Core test was trying to access actual ifc data (`ifc.get().by_type("IfcGroup")` and was failing.
2026-07-22 18:36:08 +05:00
Andrej730 f0e6cfecc1 cmake: skip compiled extensions when installing ifcwrap sources 2026-07-22 18:31:48 +05:00
Andrej730 036c74e901 downstream: dev_environment.py: detect Python 3.13 on any Blender 5.1+ 2026-07-22 17:59:38 +05:00
Andrej730 4972556eb9 downstream: ColumnPSetsOfSets.ifc: restore original schema
It seems it was switched to ifc2x3 by accident.
Related - a7738ee 6c590bf00
2026-07-22 17:26:30 +05:00
Andrej730 7338736898 ColumnPSetsOfSets.ifc: restore original schema
It seems it was switched to ifc2x3 by accident.
Related - a7738ee 6c590bf00
2026-07-22 17:24:40 +05:00
Andrej730 8f5b744cea downstream: util.schema: fix geometry_classes_introduced_after using wrong IFC4X3 schema
It was passing `IFC4X3` directly to `schema_by_name` which is expecting
schema identifier (e.g. IFC4X3_ADD2, not IFC4X3 allowed by `IFC_SCHEMA`
- IFC4X3 is one of the IFC4X3 iterations while it was in development,
not the final one).

Noticed by tests failing:
FAILED
test/util/test_schema.py::TestGeometryClassesIntroducedAfter::test_ifc4x3_to_ifc2x3_is_superset_of_ifc4_to_ifc2x3
- RuntimeError: No schema named IFC4X3
FAILED
test/util/test_schema.py::TestGeometryClassesIntroducedAfter::test_ifc4_to_ifc4x3_is_empty
- RuntimeError: No schema named IFC4X3
2026-07-22 16:26:49 +05:00
Andrej730 e9e2f89649 Revert "Sync ifcopenshell_wrapper.pyi with sync_stub.py"
This reverts commit b61f809731.

This commit was probably using not updated build, currently latest build is e333c1c and can confirm that it has `logger_or_root` added and `delete_same_facet_edge_pairs` removed.
2026-07-22 15:19:16 +05:00
Andrej730 20b1d98178 downstream: express: drop Python 2 fallbacks 2026-07-22 14:52:28 +05:00
Andrej730 c5634d5160 downstream: express: fix use of non-existent ifcexpressparser
`express.bnf` arg wasn't handled since d506ad77b
`ifcexpressparser` waa moved inside `ifcopenshell-python` awhile ago too
2026-07-22 14:52:28 +05:00
Andrej730 89663b716a downstream: pyparsing: fix using deprecated aliases
Deprecated since pyparsing 3.0 and produce runtime warnings. New
function work exactly the same, except their name is pep8 compatible.
2026-07-22 14:52:28 +05:00
Andrej730 9da1b9e02b downstream: express_parser: fix non-idempotent results 2026-07-22 14:52:28 +05:00
Andrej730 94a7ca6ac5 downstream: express: clean up trailing spaces 2026-07-22 14:52:28 +05:00
Andrej730 90c395bde4 downstream: express: update transpiled express rules using latest Python's AST
AST parser has changed a bit and there are some minor differences in the
.py output. Updating files just to avoid seeing these diffs when
rerunning rule compiler.
2026-07-22 14:52:28 +05:00
Andrej730 9e6179e671 downstream: rule_compiler: fix error running on Python 3.14
Example error:
```
    ast.Str(s=node.attr),
    ^^^^^^^
AttributeError: module 'ast' has no attribute 'Str'
```

`ast.Str` was deprecated since 3.8 and was removed in 3.14, see
https://docs.python.org/3/whatsnew/3.14.html#id9
2026-07-22 14:52:28 +05:00
Stephen Boddy a16ea85610 downstream: Remove unused imports flagged by ruff
Fixes 23 unused-import violations, mostly in the alignment API module.
2026-07-22 12:32:05 +05:00
Andrej730 38c1f32fc5 downstream: pyproject: add more packages to dev-setup 2026-07-22 12:32:05 +05:00
Andrej730 584e1b2994 downstream: pyproject: add dev-setup poe task to setup environment for ide 2026-07-22 12:32:05 +05:00
Andrej730 d5f7f616f0 downstream: bonsai pyproject: move pytest deps to requirements-dev.txt 2026-07-22 12:32:05 +05:00
Andrej730 79aa36b751 downstream: pyproject: Move tool deps from to requirements-tools.txt
Because uv was always trying to install when starting a venv in `ifcopenshell` folder, though they might be already available globally. And also they were listed twice - in pyproject and in the ci-lint.yml, now there's a single source of truth.
2026-07-22 12:30:35 +05:00
Petru Conduraru 4ceadd8f10 Fix IfcFooting Qto_FootingBaseQuantities axis mapping per predefined type #4783
Footings are authored two ways with different local axis conventions. Beam-like
footings (STRIP_FOOTING, FOOTING_BEAM) are a profile extruded along local Z, so
Length is local Z and the cross section sits on local X (Width, horizontal) and
local Y (Height, vertical). Slab-like footings (PAD_FOOTING, PILE_CAP) have their
footprint on local X/Y and their thickness (Height) on local Z.

The engine rule set is keyed per IfcFooting and cannot branch on predefined type,
so the previous static rule (Height=net_get_z, Length=net_get_max_xy, Width=null)
swapped Length and Height for beam-like footings and never emitted Width.

Add predefined-type-aware get_footing_length/width/height to the IfcOpenShell and
Blender calculators, and point the IfcFooting rule at them in all four IFC4/IFC4X3
ios/Blender rule files.

Confirmed by authoring footings through the real Bonsai generators and measuring
world-axis orientation: a beam-like footing with a 0.3 wide by 0.6 tall cross
section and 6.0 run reports Length 6.0, Width 0.3, Height 0.6, with the 0.3
physically horizontal and 0.6 physically vertical; a 2.0x1.5x0.3 pad reports
Length 2.0, Width 1.5, Height 0.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 07:05:04 +02:00
falken10vdl 0cfc77030d Merge pull request #8843 from IfcOpenShell/bonsai-6680-material-rename
Bonsai: right-click Rename Material on material rows (#6680)
2026-07-21 23:31:03 +02:00
Petru Conduraru efac8a0ec0 ifc5d: measure openings in their real orientation on both take-off engines
See #6835. Qto_OpeningElementBaseQuantities came out axis-scrambled for
openings authored in a Z-up local frame (X along the voided wall, Y
through it, Z vertical), which is how Bonsai authors every wall opening:

- The IfcOpenShell engine mapped Height to the local Y extent and Depth
  to the local Z extent, so a 0.9 x 2.0 door opening with Bonsai's
  default 1.2m void depth reported Height 1.2 and Depth 2.0, and Area
  (max side area) picked the through-wall side, 2.4 instead of 1.8.
  This matches the wrong Height=1.2/Area=1.2 screenshots reported for a
  1x1 window opening in #6835.
- The Blender engine mapped opening Width to get_length, which returns
  the longest bounding box edge, i.e. the opening height for typical
  door openings (the same defect 4adaf0d fixed for IfcDoor Width), and
  get_opening_depth used min(x, y), which returns the opening width
  whenever the width is smaller than the void depth.

The IfcOpenShell engine now has opening-aware internal calculators
(get_opening_width/height/depth/area) that detect horizontal (slab
style) openings with the same heuristic as the Blender calculator, so
slab opening depths keep reporting the slab thickness. The Blender
ruleset uses get_x for opening Width, and get_opening_depth measures the
through-element Y extent for vertical openings.

Door and window quantities themselves are addressed separately: the
Blender engine door Width was fixed in 4adaf0d, and the remaining
door/window defects (door not quantified on the IfcOpenShell engine,
inflated areas) are fixed by the attribute-based calculators in #8389.

Generated with the assistance of an AI coding tool.
2026-07-21 21:58:35 +02:00
Petru Conduraru 7ab0628c54 Bonsai: refresh material data unconditionally instead of forcing a redraw
falken10vdl reviewed 16b1b4e7b1 on #8843 and pointed out that tagging
every area for redraw was overkill. The actual problem was that the
Object Material panel and the scene Materials list read from plain
python caches (ObjectMaterialData and MaterialsData) that only get
invalidated when the Materials editing UI list is reloaded, which
never happens while you are not in editing mode. The redraw itself was
never the issue, closing the rename dialog already triggers one.

Removed the tag_redraw loop from RenameMaterial and instead call the
existing bonsai.bim.module.material.data.refresh() function from
core.rename_material, unconditionally, through a new tool.Material.refresh()
method. This is the same invalidate-on-next-load mechanism already used
by every other module's Data classes, just wired up for this operator
too, instead of introducing a new one.

Also updates the core tests to prescribe the new unconditional refresh()
call, and adds tool-layer coverage for tool.Material.refresh().

Generated with the assistance of an AI coding tool.
2026-07-21 21:08:21 +03:00
Petru Conduraru 16b1b4e7b1 Bonsai: refresh the UI after renaming a material
theoryshaw tested #8843 and asked for the new name to show up right
away instead of needing a manual refresh. The Object Material panel
and the scene Materials list both already re-read live IFC data on
their next draw (tool.Ifc.Operator purges those caches after every
IFC-mutating operator), so the button text was correct on the next
redraw. What was missing was the redraw itself: the material name is
a plain button label, not an RNA property Blender tracks, so nothing
told the Properties editor to repaint after the rename dialog closed.
Tag every area for redraw once the rename completes, the same pattern
used elsewhere in Bonsai for popup-triggered edits that need an
immediate repaint.

Also adds core-layer test coverage for rename_material, which had
none.

Generated with the assistance of an AI coding tool.
2026-07-21 17:11:56 +03:00
Petru Conduraru 8c667b8ae0 Bonsai: right-click rename on a material name (#6680)
Adds a "Rename Material" entry to the context menu that already
extends every button in the properties editor (UI_MT_button_context_menu),
triggered when right-clicking a material name button
(bim.select_by_material) that points to a real IfcMaterial. This
gives a quick entry point to renaming from the Object Material panel
without navigating to the scene Materials list.

This follows the pattern that #6680's thread converged on: theoryshaw
requested a right-click entry (rather than a pencil icon or
double-click) that keeps the existing single-click select-by-material
behaviour intact. falken10vdl is the issue's assignee; this is offered
as a starting point for that discussion, not a replacement for it.

Generated with the assistance of an AI coding tool.
2026-07-21 15:54:59 +03:00
Ryan Schultz e52e5e2e58 Bonsai: add category-level select-all to the Drawings list (#8826)
Add an "Is Selected" checkbox to each target-view category header in
BIM_UL_drawinglist that toggles selection for all drawings in the
category. The toggle only affects drawings currently visible in the
list (honoring the show_drawings_on_sheets_only filter), and the header
checkbox reflects the aggregate selection state of its drawings.

Also make category headers more obvious: wrap them in a box() for a
distinct inset background and make the header name clickable to
expand/contract the category (same as the disclosure triangle).

Ref: #8825

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:39:22 -05:00
Ryan Schultz 2d59ea1988 Bonsai: add toggle to show only drawings placed on sheets (#8824)
Adds a "Show Only Drawings on Sheets" toggle below the drawing list. When
enabled, the list is filtered to drawings referenced by at least one sheet
(target-view headers with no sheeted drawings are hidden too), and
bim.select_all_drawings only acts on the visible/filtered drawings.

A drawing is considered sheeted when its drawing document Location matches a
document reference Location on any SHEET-scoped IfcDocumentInformation.
Filtering is computed live so it reflects sheet edits without reloading.

Closes #8823

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:03:56 -05:00
Petru Conduraru 55a2430d71 docs: cover the Blender 5.1 / Python 3.13 transition in installation guides (#8781)
* docs: cover the Blender 5.1 / Python 3.13 transition in installation guides

The system requirements still listed Blender 4.3-4.5 with Python 3.11
only, and nothing documented the pitfall from issue 7623: importing
preferences into a Blender whose Python version changed carries over an
incompatible Bonsai build that silently fails to load. Document the two
Python generations, that Get Extensions picks the matching build
automatically while manual zip installs do not, and the
uninstall-reinstall step that resolves the upgrade case.

Generated with the assistance of an AI coding tool.

* docs: keep it simple, only Blender 5.1 and 5.2 with Python 3.13

Per review, drop the descriptive text and the Python 3.11 line.

Generated with the assistance of an AI coding tool.
2026-07-20 16:27:51 +10:00
Petru Conduraru 04a2535a98 Preserve the real cause when the ifcopenshell wrapper fails to load (#8785)
* Keep real cause in wrapper ImportError

When the compiled wrapper exists for the current interpreter but fails
to load (for example a glibc version mismatch, as on AWS Lambda in
issue 5927), the bare except rewrote the error into the misleading
"IfcOpenShell not built for '<platform>'" message. Environments such
as AWS Lambda or the Blender add-on dialog only surface the final
exception message, so the actual cause was invisible and undiagnosable.

Keep the "not built for" message only when no matching binary is
present, and otherwise include the original loader error, chaining the
cause in both branches.

This change was AI-generated.

Fixes #5927

* Simplify wrapper import failure to a single message

Per review feedback, drop the filesystem scan and the two message
variants. Always raise the classic "IfcOpenShell not built for
'<platform>'" message with the original exception appended in
parentheses, still chained as the cause. Environments that only show
the final exception message (AWS Lambda, the Blender add-on dialog)
now surface the real loader error, such as the glibc version mismatch
in issue 5927, without any extra logic.

This change was AI-generated.
2026-07-20 16:27:22 +10:00
Petru Conduraru 248c7e28c9 Remove QtViewer remnants
Per aothms's request on #8605: QtViewer is being superseded by the new
Bonsai Viewer, so its remains are deleted here (src/qtviewer, its
BUILD_QTVIEWER cmake option and add_subdirectory, and its references in
ci.yml's path filter, .gitignore, the conda recipe's license table, and
README's library table).

src/ifcopenshell-python/ifcopenshell/geom/app.py's qtViewer3d is
unrelated (pythonocc-core's own OCC.Display widget class, a name
coincidence) and is untouched.

Generated with the assistance of an AI coding tool.
2026-07-20 10:46:20 +10:00
Petru Conduraru 727b5f3475 Bonsai: add Hour zoom level to the interactive Gantt chart
The jsGantt-improved library that renders Bonsai's Gantt chart already
ships full support for an "Hour" granularity (column width, header
labels in every bundled language, hour-aware rendering math). Bonsai's
config only exposed Day/Week/Month/Quarter, with a comment claiming
Hour caused browser issues even with vUseSingleCell enabled.

Headless Chrome testing against the same library version shows that
claim no longer holds once vUseSingleCell is active (as Bonsai already
configures it at 10000): Hour-format charts render without errors from
typical schedules up through fairly extreme ones (5000 tasks across a
3 year span rendered in about 2.4s). The failure mode the old comment
described only reproduces with vUseSingleCell disabled, which is not
how Bonsai runs it.

Task start/finish times already flow through to the chart unmodified
as raw ISO datetimes (tool/sequence.py create_new_task_json), so any
schedule authored with real hour-level timestamps, for example an
imported MS Project/P6/Excel schedule or one written directly through
ifcopenshell-python, can now be viewed at hour granularity. Verified
live with a night shift schedule crossing midnight, rendered correctly
with no console errors.

Note: Bonsai's own "Edit Task Time" UI currently always snaps
ScheduleStart/ScheduleFinish to 09:00/17:00 regardless of the hour
entered (ifcopenshell/api/sequence/edit_task_time.py), and work
calendars only encode working days, not working hours. So authoring a
genuine hour-precision schedule through that UI is still not possible;
this change only unlocks viewing hour-level data that already exists
in the model. Fixing the editor and calendar model is a separate,
larger design decision for a maintainer.

Addresses #2772.

Generated with the assistance of an AI coding tool.
2026-07-20 10:19:42 +10:00
Bartok a45f2fae61 docs(ifc2ca): fix script paths in README
Point scriptSalome.py at templates/salome/ and the bonded scripts at
_deprecated/, matching the current tree so README links resolve.

Generated with the assistance of an AI coding tool.
2026-07-20 09:53:35 +10:00
dependabot[bot] 1ae50b8cce build(deps): bump ruff from 0.15.12 to 0.15.22 (#8212)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.12 to 0.15.22.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.12...0.15.22)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 09:52:46 +10:00
dependabot[bot] 9fda996ebe build(deps): bump ruff from 0.15.12 to 0.15.22 (#8497)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.12 to 0.15.22.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.12...0.15.22)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 09:52:38 +10:00
dependabot[bot] 7a7a250942 build(deps): bump ruff from 0.15.12 to 0.15.22
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.12 to 0.15.22.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.12...0.15.22)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-20 09:52:07 +10:00
Petru Conduraru bc1fb2a88d Bonsai: add one click copy of annotations to another drawing (#8719)
* Bonsai: move annotations between drawings when reassigning their group

Assigning an IfcAnnotation to a group that represents another drawing
previously left the annotation in both drawings at once: it stayed in
its old drawing group, its Blender object stayed in the old drawing
collection, and it kept the old camera depth, so the reassignment
appeared to do nothing useful. Issue #2966 documents the seven step
manual workaround users needed instead.

The assign group operator now detects when the target group represents
a drawing (via the new tool.Drawing.get_group_drawing, the inverse of
get_drawing_group), unassigns the annotation from its previous drawing
group, moves its object into the new drawing collection, and places it
on the new drawing camera plane. The target camera is imported on
demand when it has not been loaded yet, matching the pattern used by
the activate drawing operator.

Generated with the assistance of an AI coding tool.

* Bonsai: add one click copy of annotations to another drawing (#2966)

Duplicating an annotation into a different drawing used to require a
seven step manual process: loading groups in scene properties, copying
the object, fixing its group assignment by hand, and repositioning it
onto the target camera plane. A plain Blender duplicate is not enough
because the copy keeps pointing at the same IFC entity, and the Shift D
override, while it does create a genuine new entity through
root.copy_class, leaves the duplicate in the source drawing group,
collection, and camera depth.

The new copy annotation to drawing operator packages the proven recipe
already used by duplicate drawing into one action: duplicate through
tool.Geometry.duplicate_ifc_objects, unassign the copy from the source
drawing group, assign it to the chosen target group, place it on the
target camera plane at the same world XY, and file it into the target
drawing collection. The originals are left untouched and the user's
selection is restored. The target camera is imported on demand when it
has not been loaded yet.

The operator shows a target drawing dropdown and is reachable from the
annotation tool sidebar when an annotation is selected, and from the
drawings panel. Annotations already in the target drawing are skipped
and reported.

The orchestration lives in core.drawing.copy_annotations_to_drawing
with prophecy tests covering the copy, the skip, and the camera import
branches. Verified live in headless Blender 5.1: the copy is a new
IfcAnnotation with its own GlobalId and IfcTextLiteral, both texts are
editable independently, and everything survives save and reload with
each annotation loading in its own drawing.

Generated with the assistance of an AI coding tool.
2026-07-20 09:50:14 +10:00
Petru Conduraru c55a79b8b5 bonsai: allow overriding which classes join in section linework (#4395) (#8617)
Fixes #4395.

Root cause: the SVG cut-linework merge step that fuses adjacent
elements' cut polygons together (per the pset-driven JoinCriteria
setting) was hardcoded to only IfcWall and IfcSlab. IfcCovering cut
shapes were skipped unconditionally, so adjacent coverings never
joined, leaving a visible seam/broken corner in section drawings
regardless of JoinCriteria.

Fix: added an EPset_Drawing.JoinClasses property, following the
exact same user-overridable pattern already used by
EPset_Drawing.BringToFront - a comma-separated list of IFC classes
to join, defaulting to "IfcWall,IfcSlab" (unchanged behavior) when
unset. Users can override per-drawing to add IfcCovering (or any
other class) when they want it joined too. Kept this opt-in rather
than hardcoding IfcCovering into the default list, since joining a
thin finish layer the same way as a thick wall/slab could produce
unwanted mitring in some cases - the user decides per drawing.

Verified live against the reporter's own attached file
(ifcovering joining.ifc) and its cached section linework: with
JoinClasses unset, two separate closed paths reproduce the reported
seam exactly. With JoinClasses = "IfcWall,IfcSlab,IfcCovering", the
two coverings merge into a single closed polygon with the internal
seam removed. Confirmed IfcSlab join behavior is unchanged in both
runs.

Generated with the assistance of an AI coding tool.

Co-authored-by: Dion Moult <dionmoult@gmail.com>
2026-07-20 09:41:09 +10:00
sboddy b669baf793 Propagate deflection settings on reload (#8484)
reimport_element_representations() built a fresh
ifcopenshell.geom.settings() without copying deflection_tolerance /
angular_tolerance from the IfcImportSettings it had just
constructed, and never passed geometry_library to either the
iterator() or create_shape() calls it makes. As a result, exiting
Item/edit mode (which reaches this function via
switch_representation) silently fell back to IfcOpenShell's
hard-coded mesher defaults (0.001 linear deflection, ~50x finer than
the project's default of 0.05) and the default geometry kernel,
instead of the project's configured tolerance and Geometry Library.

This made geometry visibly change quality after a no-op Tab into and
back out of edit mode, since the reload path was unintentionally far
more precise (and used a different kernel) than the initial import.
Both settings, and geometry_library, are now taken from the
IfcImportSettings instance already built at the top of the function,
so a reload matches the original import.

Refs #5685.

Generated with the assistance of an AI coding tool.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 09:37:04 +10:00
Petru Conduraru 32ac20e8e3 Bonsai: refresh the arc/circle decorator immediately after duplicating a loop
theoryshaw's follow-up on #6944: after the profile/curve reconstruction fix
(previous commit), the arc/circle marker for a freshly Shift+D-duplicated
loop wouldn't appear until leaving and re-entering Edit Mode.

Root cause: ProfileDecorator groups arc/circle vertices purely by
IFCARCINDEX/IFCCIRCLE vertex-group index every draw call (it has no cache
to go stale, it fully recomputes from the live edit-mesh bmesh each frame).
Duplicating a loop copies its vertex-group weights onto the new geometry,
since Blender allocates no new group for a duplicate, so the source loop
and its live duplicate land in the same dict entry. That entry then fails
the "exactly 2 verts per circle / 3 per arc" check and is skipped entirely,
so BOTH the original and the duplicate stop being drawn until the mesh is
reimported and gets fresh, distinct groups.

Verified live in headless Blender: built a bmesh with an IFCCIRCLE loop and
an IFCARCINDEX loop, then ran bmesh.ops.duplicate on each (the same
bmesh-level operation underlying Shift+D) and called ProfileDecorator's
draw method directly. Before this change, duplicating either loop dropped
both the original and the duplicate from the decorator (0 circle/arc
batches drawn instead of 2). After, both draw immediately, with no change
to the non-duplicated case (still 1) or to genuinely distinct loops (5
independent circles still resolve to 5, not merged). 500-circle timing is
unchanged (~14.3ms/draw before and after), so the added connectivity split
is not a hot-path regression.

Added test/bim/module/model/test_profile_decorator_duplicate_loop.py
pinning the new _connected_components helper's behavior for single and
duplicated circle/arc loops.

This contribution was produced with the assistance of an AI coding tool.
2026-07-20 09:34:07 +10:00
Petru Conduraru 0d5ea02169 Bonsai: fix the same duplicate-loop vertex-group bug in auto_detect_curves
auto_detect_profiles had the identical defect fixed in the previous
commit: duplicating a circle/arc loop in Edit Mode reuses the same
IFCCIRCLE/IFCARCINDEX vertex group index for the new geometry, and this
sibling function (used for curve/annotation editing rather than profile
voids) tallied group membership across the whole mesh instead of per
loop, so it also rejected a legitimately duplicated loop as malformed.

Applied the identical fix: scope the group-count sanity check to each
connected edge loop, computed after the loops are built rather than in
the initial whole-mesh vertex pass. Kept the existing forked-loop check
(more than 2 edges per vertex) in the first pass since it is unrelated
to group counting.

Verified live in headless Blender: constructed two 2-vertex IFCCIRCLE
loops sharing one vertex group index (the exact state Blender's Edit
Mode duplicate produces) and called auto_detect_curves directly.
Before this change it returned (False, "CIRCLE"); after, it returns two
valid IfcCircle curves.

Generated with the assistance of an AI coding tool.
2026-07-20 09:34:07 +10:00
Petru Conduraru 0a027d47a3 Bonsai: fix profile reconstruction after duplicating a circle/arc in Edit Mode
Duplicating a circular or filleted-arc void in the profile CAD editor
(Shift+D on the loop's vertices) reused the same IFCCIRCLE/IFCARCINDEX
vertex group index for the new geometry, since Blender's mesh duplicate
copies vertex group weights but does not allocate a new group. On exit
from Edit Mode, auto_detect_profiles tallied group membership across the
whole mesh rather than per loop, so a group meant to hold exactly 2 (circle)
or 3 (arc) vertices ended up with double that, failing its sanity check
and blocking the edit with an "INVALID PROFILE" popup. Fixes #6944.

Scope the sanity check to each connected edge loop instead, matching how
the loops are actually converted into IfcCircle/arc segments below. Also
explicitly reject an arc/circle vertex tagged onto an isolated vertex with
no edges at all, which the old whole-mesh count also caught.

Verified live in headless Blender against the issue's repro file
(IfcFurniture "Slab.004", IfcArbitraryProfileDefWithVoids with three
IfcCircle voids): entering the profile editor, duplicating one void's
2-vertex loop and moving it produced an "INVALID PROFILE" popup before
this change, and now produces a valid profile (the original 3 voids
intact, plus the duplicate as a 4th void or a separate solid profile
depending on whether it still falls inside the outer boundary).
test/tool/test_model.py passes unchanged (32 passed, 1 pre-existing
unrelated failure present on both before and after).

Generated with the assistance of an AI coding tool.
2026-07-20 09:34:07 +10:00
Petru Conduraru 6014bbd877 ci-lint: black-format two files that drifted on v0.8.0
Both files were merged unformatted and fail the Black formatter step
on every branch, keeping ci-lint red repo-wide.

Generated with the assistance of an AI coding tool.
2026-07-20 09:24:34 +10:00
Petru Conduraru 6d6d92b849 Fix all remaining ty type-check failures on ci-lint
The ci-lint workflow's ty steps fail on every branch because base
v0.8.0 has four diagnostics.

ty check (bonsai):

- root/operator.py: bpy.data.objects.get() can return None, so
  UnlinkObject._execute could put None in its objects list and crash
  on the first attribute access when an unknown object name is passed.
  Handle the miss explicitly, which also satisfies the declared
  list[bpy.types.Object] type.
- tool/sequence.py: ty does not narrow Literal types through
  membership tests on list literals, so the assert_never() exhaustive
  check was flagged. Use tuple literals, which ty narrows, keeping the
  exhaustiveness check intact.

ty check (ios):

- draw.py: arrange_polygons was called through conditional argument
  splats that let the same call site work against pre-April-2026
  wrappers lacking arrange_polygon_settings and the logger parameter.
  No runtime bug for current builds, but the dynamic splats cannot be
  typed against the fixed 3-parameter signature. Drop the old-build
  workaround and call the current signature directly, following the
  precedent of 3d8115ebc5 which dropped similar old-build workarounds
  in ifcopenshell.file. Verified against a current wrapper build that
  the direct call arranges polygons and serializes to SVG, with and
  without a logger.
- Optimise.py: igraph is an optional dependency with a guarded import
  and a toposort fallback, but it was missing from the ios type-check
  venv so ty could not resolve it. Add it to type-check-requirements
  next to the toposort fallback that is already listed.

After this, poe ty-bonsai and poe ty-ios both pass cleanly.

Generated with the assistance of an AI coding tool.
2026-07-20 09:23:48 +10:00
Petru Conduraru bb49822f2e Bonsai: make dxf2ifc.py example script skip unsupported DXF entities
The script called Polyline.get_mode() on every modelspace entity, but
that method only exists on POLYLINE entities, so any typical DXF
containing lines, circles or text crashed with AttributeError before
converting anything. Test for POLYLINE polyface meshes with
dxftype()/is_poly_face_mesh instead and skip other entities with a
message, only create the spatial containment relation when products
exist, and take the input/output paths from the command line (matching
obj2ifc.py) instead of a hardcoded input.dxf/test.ifc.

Fixes #2151

This change was written with the assistance of an AI coding tool.
2026-07-20 09:22:44 +10:00
Bruno Postle 21ea58b0e6 Fix Bonsai polyline not enough values to unpack error
Typo was introduced in b35f99e
2026-07-19 23:50:19 +01:00
Ryan Schultz b66d8b2c4d Fix #6652: Extend grab selection to include BBIM_Array members (#7968)
When grabbing an array child, the selection now expands to include
the array parent and all sibling children before the move operator
runs. Mirrors existing behavior for aggregates and nests.

Generated with the assistance of an AI coding tool.
2026-07-19 14:00:44 -05:00
Stephen Boddy f9be61c10b Bump build 821cf7b > e333c1c 2026-07-19 18:47:57 +01:00
Stephen Boddy b61f809731 Sync ifcopenshell_wrapper.pyi with sync_stub.py
Ran the new sync_stub.py against a real local build: adds
context.delete_same_facet_edge_pairs (present on the compiled wrapper,
missing from the stub) and drops the module-level logger_or_root
(present in the stub, no longer exists on the wrapper at all).

Nothing else changes - no license header rewrite, no docstring loss,
none of the 14 hand-curated named-parameter constructor/function
signatures touched, unlike the wholesale regeneration this replaces.

Generated with the assistance of an AI coding tool.
2026-07-19 16:13:13 +01:00
Stephen Boddy 948ffce7e9 Add sync_stub.py, a minimal-diff stub syncer
generate_stub.py (this branch's earlier commit) regenerates
ifcopenshell_wrapper.pyi wholesale from the compiled wrapper: it
reliably fixes real drift, but it also discards everything that isn't
mechanically recoverable from the wrapper alone - the license header,
docstrings, and hand-curated named-parameter signatures for
SWIG-overloaded constructors/functions (SWIG itself always emits
generic `*args` for those, so a regenerator can't tell a deliberate
curation from real drift and just overwrites it).

sync_stub.py takes the smaller-blast-radius approach: it only adds
top-level symbols/class members that are genuinely missing, and only
removes ones that are genuinely gone, cross-checking against
validate_stub.py's own full canonicalisation (via the newly-exposed
get_names_tree()) so it never mistakes a property()/staticmethod()-
wrapped member for something absent just because its own narrower
parser skips that form. Anything that exists on both sides under the
same name but with a different signature - exactly where curation
lives - is left untouched and reported for a human to review instead
of guessed at.

Verified against a real local build: applying it to the current
ifcopenshell_wrapper.pyi produces a small, targeted diff (add one
missing method, drop one stale function) with the license header,
docstrings, and all 14 curated constructor/function signatures
preserved byte-for-byte, versus generate_stub.py's ~1000-line
wholesale rewrite for the same underlying fix.

Generated with the assistance of an AI coding tool.
2026-07-19 16:13:13 +01:00
Petru Conduraru f23db9440f ifcparse: widen all integer attribute types to int64_t for consistency
Follow-up to the scalar-only fix in #8754, per aothms's direct request on
that PR ("Please do make all int types consistent") and his own original
2023 design intent on issue #3058 ("make all integers (incl. schema
namespaces) an int64_t"). Widens the remaining inconsistent spots now that
compatibility isn't a constraint on this v0.9 branch:

- Integer aggregates (IfcTriangulatedFaceSet.CoordIndex and similar
  List<int> attributes), including the SWIG to_vec_int/to_vec_vec_int
  helpers, which previously silently truncated via static_cast<int> on the
  Python-set path - the same bug class as the original scalar issue.
- The schema code generator (express/mapping.py's integer type mapping),
  and all 12 generated schema header/source pairs regenerated to match, so
  every schema-typed getter/setter (e.g. IfcOwnerHistory::CreationDate) is
  int64_t end to end, not just the dynamic attribute-value path.

Instance/reference identifiers (STEP #123 ids) are deliberately left at
32-bit: they're a file-local index into internal maps, not an EXPRESS
domain value an application chooses, and no realistic STEP file has
billions of entities. The lexer's Token_IDENTIFIER parsing still funnels
through a 32-bit int for this reason - flagged as a known, low-risk gap
rather than fixed, since fixing it would mean touching indexing/hashing
code for no realistic benefit.

Verified: original PR's round-trip tests extended with aggregate cases
(IfcTriangulatedFaceSet.CoordIndex, InnerCoordIndices) at 64-bit boundary
values, in memory and through STEP text, IFC2X3 and IFC4. A standalone C++
program exercising the generated schema API directly (Ifc4::IfcOwnerHistory
::setCreationDate/CreationDate, IfcTriangulatedFaceSet::setCoordIndex/
CoordIndex) confirms int64_t end to end, bypassing SWIG. Full build
(BUILD_IFCGEOM, WITH_OPENCASCADE, BUILD_IFCPYTHON, IFC2X3+IFC4) clean.
test/util/test_attribute.py and test_file.py pass unchanged.

This contribution was produced with the assistance of an AI coding tool.
2026-07-19 13:54:16 +02:00
Petru Conduraru d5076bded3 ifcparse: store integer attribute values as int64_t to allow out-of-range timestamps
Setting an IfcInteger/IfcTimeStamp typed attribute (e.g. IfcOwnerHistory.CreationDate)
outside the signed 32-bit range corrupted the value instead of raising, since the
Python wrapper's set_attribute_value_py() truncated it with a plain static_cast<int>
before handing it to the C++ storage. Unix timestamps before 1901-12-13 or after
2038-01-19 silently wrapped around (e.g. 3000000000 became -1294967296) rather than
being rejected or stored correctly. Fixes #3058, equivalent to PR #8683 but ported to
this branch's rewritten ifcparse (snake_case files, variant_array/instance_data
storage, SWIG PyObject-based attribute setter) instead of the old IfcEntityInstanceData
sources, which no longer exist here.

The scalar slot of the attribute variant (Argument_INT) becomes int64_t. Integer
aggregates (Argument_AGGREGATE_OF_INT, e.g. CoordIndex) and instance/reference
identifiers stay 32-bit, since neither is the value that overflows here; this narrow
scope is kept on its own technical merits (aggregates and identifiers were never the
source of the bug, and widening them would be a much larger, riskier change for no
benefit) even though aothms said compatibility isn't a concern on this v0.9-track
branch. express::Base::set_attribute_value promotes the schema-generated int to
int64_t at a single choke point, so the generated setters keep compiling unchanged.
The STEP lexer, writer, and SWIG wrapper (set_attribute_value_py, pythonize) are all
widened together, since widening only the Python-facing setter would have silently
wrapped the value on file write instead of raising.

Verified in a build (IFC2X3 and IFC4, BUILD_IFCGEOM off, no kernels): pre-1901,
post-2038, both 32-bit boundaries, and a 9e12 value all round trip exactly both in
memory and through STEP text serialization (write then reopen). A value outside the
64-bit range now raises a clean exception instead of corrupting data. Ordinary
in-range integers and integer aggregates (e.g. IfcTriangulatedFaceSet.CoordIndex) are
unaffected. The existing util/test_attribute.py and test_file.py suites pass
unchanged; test_entity_instance.py has 5 pre-existing failures unrelated to this
change (confirmed identical on an unfixed build of this branch, caused by a missing
get_info_2 binding and _patch_swig_comparisons never being implemented here).

Generated with the assistance of an AI coding tool.
2026-07-19 13:54:16 +02:00
Petru Conduraru c68e4a0eee Size entity attribute storage to schema arity, not token count
When a STEP instance has fewer attribute tokens than its schema declares
(commonly from corrupted/malformed syntax), parse_context::construct()
sized the in-memory attribute storage to the smaller token count instead
of the schema's attribute count. This left the storage's last N attribute
slots simply nonexistent rather than blank, so any later read of one of
those trailing attributes by index threw an uncaught IfcParse::IfcException
("Index N is out of range for storage of size N") that terminated the
whole process (SIGABRT) instead of being handled as a parse warning.

Fix: when the schema declaration is known, size the storage to the
schema's attribute count. Indices beyond the number of tokens found are
left at their existing default-constructed blank value (the storage
constructor already blank-initializes every slot), so a truncated
instance now degrades to blank values for its missing trailing
attributes, matching the parser's existing "expected N attribute values,
found M" warning intent instead of crashing.

Reproduced with the fuzzing script attached to #5679: single-byte
mutations of a minimal IFC4 file that corrupt the IFCPROJECT instance's
token stream reliably aborted IfcConvert with this exact exception before
the fix, and now parse with a logged syntax error and exit code 0.

Fixes #5679

Generated with the assistance of an AI coding tool.
2026-07-19 10:29:48 +02:00
Petru Conduraru 6603c8459a Fix pythonocc-core viewer compatibility in geom.occ_utils and geom.app (#1037, #1098)
set_shape_transparency() called AIS_InteractiveContext.SetTransparency(),
whose argument count is inconsistent across pythonocc-core versions
(reported as a TypeError in #1037). Set transparency directly on the AIS
object instead, the same stable pattern already used elsewhere in this
file (display_shape() calls ais.SetTransparency() directly, never through
the Context), then call Context.UpdateCurrentViewer() to refresh.

app.py's viewer used a "SetSelectionPriority(counter)"/"SelectionPriority()"
pair as an ad hoc unique key to map a displayed AIS object back to its IFC
product. On modern pythonocc-core this crashed with AttributeError because
.GetObject() (needed to unwrap the old handle-based API) no longer exists
on AIS objects (#1098, PR #1113 partially patched one of the two call
sites but left the one in HandleSelection unguarded).

Live pythonocc-core 7.9.3 testing showed the GetObject() guard alone is
not sufficient: SetSelectionPriority/SelectionPriority themselves have
been removed from AIS_InteractiveObject entirely in modern OCCT (only
AIS_Trihedron keeps a same-named but unrelated method for datum parts),
so gating the .GetObject() call with the existing USE_OCCT_HANDLE flag
would still crash the first time a shape is selected. Verified live that
AIS objects retain correct __eq__/__hash__ (matching the underlying OCCT
instance) across separate SWIG wrapper instances, so ais_to_product is
now keyed directly by the AIS object itself, removing the dependency on
the removed OCCT API and the GetObject()/handle distinction altogether.

Verified live against pythonocc-core 7.9.3 (conda-forge) using real
AIS_Shape objects obtained from ifcopenshell.geom.occ_utils.display_shape()
and a real IFC file: reproduced both the original TypeError (#1037) and
AttributeError (#1098), confirmed both fixes resolve them, and confirmed
the ais_to_product dict lookup round trips correctly through a real
Context.Select()/SelectedInteractive() call. Could not exercise the full
Qt-embedded viewer.finished()/HandleSelection() flow end to end because
this pythonocc-core build segfaults natively when creating a second GL
context inside a Qt widget on this macOS host, a pre-existing environment
issue unrelated to this diff (reproduces identically with unpatched code,
before any touched line executes).

AI-generated, reviewed and tested by Petru Conduraru.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 10:10:09 +02:00
Petru Conduraru 824c1fc280 ifcwrap: keep geometry's owning element alive to fix silent data corruption (#1124)
create_shape() returns a Python-owned Element (SWIG_POINTER_OWN in the
boost::variant out typemap). Its .geometry property calls Element::geometry(),
which returns a reference into the element's boost::shared_ptr<Representation>
_geometry member. SWIG wraps that reference as a non-owning pointer, so the
returned Triangulation/BRep/Serialization proxy does not keep the element alive.

When a caller keeps only .geometry (e.g. create_shape(s, e).geometry) and drops
the parent element, Python garbage-collects the element, destroying its
shared_ptr and freeing the underlying representation. Subsequent reads of
verts/faces then return freed memory: empty or implausible float/int garbage,
non-deterministically depending on GC and allocator timing. This is silent data
corruption, not a crash, and has bitten users since 2020.

Fix: in the TriangulationElement/SerializedElement/BRepElement pythoncode, wrap
the geometry getter so the returned geometry stores a backreference to its
owning element (result._parent = self). This makes the parent's lifetime at
least as long as the geometry's, automatically and transparently, so no caller
has to remember to hold the element. This is aothms's suggested backreference,
applied generically in the binding rather than left as a workaround.

Reproduced deterministically (washBasin fixture): before, verts len 0 vs 133500
across repeated GC-pressure runs; after, 133500 every run for all three element
types. test_create_shape passes; no regressions.

Note: tree.select_ray()'s ray_intersection_result (2024 follow-up in #1124) is a
separate ownership mechanism (std::vector element reference + std::array member
pointer) and is left as follow-up scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 10:04:48 +02:00
carlopav 705af7ba3a drawing: compute cut/fill intersection once per CutDecorator object
recalculate_cut() and recalculate_fill() each ran is_intersecting_camera(),
which builds a bmesh and scans every vertex. When a redraw recalculated both
(camera moved, cache miss, or the object selected) that was two full
intersection tests per object per frame for the same answer.

Compute it once in decorate() and pass it to both, and skip the test
entirely when neither recalculation is needed. Never more tests than before,
identical result since the camera can't move within a frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 14:15:42 +10:00
carlopav 074fc26e8f drawing: evaluate camera movement once per CutDecorator redraw
is_camera_moved() runs eval()/numpy over the camera matrix and, as a side
effect, refreshes the stored checksum the first time it returns True. It was
called up to twice per object inside decorate(), so on a frame where the
camera actually moved the first call updated the checksum and every later
call - the fill check on the same object, and both checks on all remaining
objects - then saw an already-current checksum and returned False. Only the
first object's cut got recalculated; its fill and every other element stayed
stale until something else invalidated the cache.

Evaluate it once at the top of __call__ and reuse the flag. This halves the
per-object eval overhead on the common path (viewport navigation with the
camera object stationary) and, when the camera does move, correctly
recalculates the cut and fill for every intersecting element instead of just
the first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 14:15:42 +10:00
Ryan Schultz a90064929b Bonsai: preserve occurrence geometry/material/styles when deleting a type
Deleting a type used to strip its occurrences: any that displayed the
type's mapped representation lost their geometry, and inherited material
and presentation styles were dropped too.

The no-SHIFT "Delete Type" path now bakes each occurrence's geometry,
styles, and inherited material onto the occurrence before the type is
removed:
- Refactor UnassignType's unmap logic into a reusable
  UnassignType.unassign_and_unmap(), and extend it to re-attach styled
  items (copy_deep only follows forward refs, so IfcStyledItem is lost)
  and bake down any inherited (non-owned) material.
- Add RemoveType._detach_type_material_set(): unhook the type's
  IfcMaterialLayerSet/ProfileSet association cascade-free before deletion,
  so remove_product's aggressive unassign_material never fires and the
  occurrences' layer/profile-set usages survive intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 17:32:49 -05:00
Ryan Schultz 397f13e71c Bonsai: add Delete Type button to Type Attributes panel
Adds a trash button in BIM_PT_type_attributes that deletes the relating
type via bim.remove_type. SHIFT+Click also deletes every occurrence of
the type in the project, behind a confirmation dialog showing the count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 15:26:42 -05:00
Petru Conduraru 7ebdd046b6 Optimise IfcPatch recipe: make toposort backend configurable
aothms asked for the toposort dependency ordering used by the dedup
walk to try igraph's C-backed topological_sorting() first, since it
should shave off additional time on top of the non-recursive
get_info fix. Falls back to the pure python toposort package with a
warning if igraph is not installed.

Generated with the assistance of an AI coding tool.
2026-07-18 22:13:29 +02:00
Petru Conduraru 57cfd9d1fd Fix #1043. Optimise IfcPatch recipe: avoid redundant recursive get_info
The 2020 profiling in issue #1043 found the Optimise recipe's dedup
loop spent almost all of its time in entity_instance.get_info(recursive=True):
because the topological sort already guarantees every referenced entity
is folded before the entity that references it, recomputing each
already-folded subtree's canonical value from scratch for every parent
that points to it is wasted work. Confirmed this is still exactly the
bottleneck in the current codebase, unchanged since 2020 (get_info's
recursive path still walks the whole subtree on every call).

Applied aothms's suggested fix from the issue thread: canonicalize each
entity with a non-recursive get_info, and for referenced entities substitute
the already-computed identity of their folded replacement (looked up in
instance_mapping) instead of re-expanding the subtree. Also limited the
toposort dependency graph to direct references (max_levels=1), since a
topological sort only needs direct edges, not the full transitive closure
traverse() was computing for every entity.

Benchmarked before and after on real IFC test fixtures and a larger
synthetic file with heavily shared geometry (thousands of walls sharing
a handful of profile/point subtrees, mirroring the sharing pattern
described in the issue):

- test/input/geometrygym_great_court_roof.ifc (56989 entities): 9.9s -> 1.7s
- test/input/acad2010_objects.ifc (16296 entities): 3.7s -> 0.4s
- synthetic 120083-entity fixture with heavy geometry sharing: 19.2s -> 3.4s

Verified correctness by comparing the full canonical (recursive get_info)
multiset of the optimized output between the old and new implementation on
all three fixtures: identical results, same fold counts.

Added test_Optimise.py covering the core scenario from the issue: entities
built from separate, value-identical non-rooted subtrees fold to a shared
instance, while entities with distinct values do not.

Generated with the assistance of an AI coding tool.
2026-07-18 22:13:29 +02:00
Petru Conduraru e333c1c100 ifcgeom: build the swept-area directrix from the offset curve far from origin (#4848)
IfcSurfaceCurveSweptAreaSolid regressed in 0.8 for geometry far from the
origin (for example parapets on a georeferenced building), which went
missing or glitched.

The kernel offsets the directrix toward the origin when it is far away
(mean.norm() > 1e2), storing the offset copy in a local curve variable and
setting applied_temporary_offset so the finished solid is translated back by
+mean. But the wire was still built from scs->curve, the un-offset original,
so the offset never took effect and the result was translated by +mean from
its correct location. Build the wire from curve instead. When no offset is
applied curve aliases scs->curve, so near-origin geometry is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 22:04:02 +02:00
sboddy 97a85fe5a7 Merge pull request #8608 from sboddy/feature-svg-edge-classification-3668-4
Classify projection edges in SVG elevations (#3668)
2026-07-18 20:32:37 +01:00
Bruno Postle 8ee52c466f Fix null reference bind in header parsing
references_to_resolve is never set while parsing header
entities, so binding a reference to it was UB, caught by
UBSan on any file with a header.

Generated with the assistance of an AI coding tool.
2026-07-18 21:31:46 +02:00
sboddy fe4fdd091d Merge pull request #8554 from sboddy/fixes-for-ci-tests
Fix ifcopenshell-python test drift (4 CI failures traced to root cause)
2026-07-18 20:31:05 +01:00
Bruno Postle 56121ca061 Fix null-pointer derefs in reference resolution
Two related bugs in read_from_stream's reference-resolution
loop, both reachable from malformed input:

- has_attribute_value<IfcBaseClass*> only checks the stored
  slot's type, not that it's non-null (e.g. an explicit $
  value), so the following get_attribute_value() call could
  return null and inst->declaration() crashed on it.
- byid_[ref] default-inserts (and returns) a null pointer
  when the owning instance id isn't present, which was then
  dereferenced unconditionally via ->data().

Added regression tests using the two minimized crash inputs
that found these.

Generated with the assistance of an AI coding tool.
2026-07-18 21:30:56 +02:00
Bartok 7b613a0bcc docs(readme): use https for IfcOpenShell website link 2026-07-18 20:38:02 +02:00
Andrej730 b35f99e63f ty: detect unresolved references 2026-07-18 22:39:33 +05:00
Andrej730 f744753726 settings_mixin.build_parser: fix ty == "bool" typo, should be an assignment 2026-07-18 22:39:33 +05:00
Andrej730 2e21fc5a98 assign_cost_item_quantity: fix indendation and missing values (de65e50)
`values` dictionary was missing and variables were never collected to it, so `FormulaEvaluator(values)` was always resulting in missing variable error.
2026-07-18 22:39:33 +05:00
Andrej730 ca9bbbc4a7 assign_cost_item_quantity: annotate 2026-07-18 22:39:33 +05:00
Andrej730 5994fbde27 ty: check assert_never
Had to bump `ty`, because 0.0.61 added support for `value in [A, B, C]` pattern for type narrowing.
2026-07-18 22:39:33 +05:00
Andrej730 47dc1a6c68 edit_true_north: handle unsetting case when TrueNorth is already None 2026-07-18 22:39:32 +05:00
Stephen Boddy 489084c7be Remove stale ty lint ignore directive 2026-07-18 15:03:32 +01:00
Stephen Boddy 6c590bf008 Fix schema mismatch in ColumnPSetsOfSets.ifc test fixture
The fixture declared FILE_SCHEMA(('IFC2X3')) but used
IFCPROPERTYSETDEFINITIONSET(...), a defined type that only exists in
IFC4+ (confirmed absent from the generated Ifc2x3-schema.cpp/
Ifc2x3-definitions.h, present in the IFC4 equivalents). The file's own
FILE_NAME record ('Column_4x3.ifc') suggests it was originally
exported as IFC4X3 and the schema tag was later miscopied to IFC2X3.

Traced with an instrumented parser build: on encountering the
unrecognized keyword, declaration_by_name() correctly throws
"Entity with name 'IFCPROPERTYSETDEFINITIONSET' not found in schema
'IFC2X3'", caught by the existing IfcException handler in
in_memory_file_storage::load(). The parser then falls back to parsing
the trailing (#136,#138) as a plain nested SET rather than the typed
value, so RelatingPropertyDefinition ends up as a bare tuple instead
of an IfcPropertySetDefinitionSet-wrapped value with .is_a(). This is
correct, expected behavior for content that doesn't match its
declared schema - not a parser bug. Fixing the header to IFC4 (which
does declare the type) resolves test_stream, test_file, and test_rocks
in test_streaming_rocksdb_and_simpletyperefs.py.

Generated with the assistance of an AI coding tool.
2026-07-18 15:03:32 +01:00
Stephen Boddy 96e2efebc8 Route boolean-op kernel logging through the injected Logger
src/ifcgeom/kernels/opencascade/boolean_utils.cpp, OpenCascadeKernel.cpp,
and boolean_result.cpp logged diagnostics (including the "Processed
fully in 2D" family of messages) through the global Logger::Root()
singleton. IfcConvert's main() constructs its own Logger and wires it
to --log-file via SetOutput(), then threads that instance through
Converter/kernel constructors as logger_ (see AbstractKernel). Since
Logger::Root() is never itself configured with an output stream, every
Notice/Warning/Message call through it was silently dropped instead of
reaching the log file - Logger::Message's log1_/log2_ null checks just
no-op.

This made src/ifcopenshell-python/test/test_wall_opening.py fail: it
asserts on specific log messages that the underlying boolean-op code
was still emitting correctly, just to nowhere. The geometry itself was
never wrong.

Add a Logger*, defaulting to null, to boolean_settings (with a log()
accessor falling back to Logger::Root() for the few remaining
call sites with no injected logger available), thread it through
eliminate_narrow_operands and boolean_subtraction_2d_using_builder,
and have OpenCascadeKernel/boolean_result.cpp populate it from their
inherited logger_ member instead of relying on the global singleton.

Generated with the assistance of an AI coding tool.
2026-07-18 15:03:32 +01:00
Stephen Boddy d188e3beaf Allow process/resource type assignment via Type-suffix convention
The class-pairing validation added in 10ee5aef4f rejects any type
assignment whose class isn't in the buildingSMART implementer
agreement map. That map only covers physical product occurrence/type
pairs (IfcWallType -> IfcWall, etc); IfcTypeProcess and IfcTypeResource
subtypes such as IfcTaskType, IfcProcedureType and the resource types
have no entry, so previously-valid assignments like
IfcTaskType -> IfcTask were rejected with "allowed occurrence
classes: <none>".

These classes still follow the schema's universal Type-suffix naming
convention, so derive the pairing the same way the existing
ApplicableOccurrence fallback does: strip "Type" from the relating
type's class name and accept it only if the schema actually declares
that entity. This can only add pairings implied by the type's own
class name, so it cannot loosen the existing rejection of genuine
mismatches (e.g. IfcWallType -> IfcWindow).

Generated with the assistance of an AI coding tool.
2026-07-18 15:03:32 +01:00
Stephen Boddy 93c0290131 Minor tweak to the default lining weights
The crease and sharp weighting seemed flipped to my sensibilities, so
now crease is heavier than sharp. I also added a commented out block
for debug colours in case someone wants to quickly use bright colours
to diagnose future problems.
2026-07-18 01:33:15 +01:00
Stephen Boddy f3a7a35acf Expose SVG edge classification settings in drawing UI
Add UseEdgeClassification, RenderCreases, ValleyAngleMinDegrees,
RenderSharp, RidgeAngleMinDegrees, and RenderFlush to EPset_Drawing,
following the existing HasUnderlay/DPI/PerspectiveShiftX pattern.
The master toggle defaults off, preserving current linework output;
the three dependent controls only show in the panel once it's on.

Removes the previous dormant, transient operator-redo properties for
the ridge/valley thresholds and flush-edge toggle, which were never
persisted per-drawing or exposed in any panel, replacing them with
the persistent camera properties read in setup_serialiser().

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:33:15 +01:00
Stephen Boddy 183e4c47f7 Add SVG edge classification on/off + render settings
Add svg-use-edge-classification (default off, preserving today's
linework), svg-render-crease-edges, and svg-render-sharp-edges
settings, gating the existing 5-class classification feature so it
can be disabled entirely (falling back to the pre-classification
whole-shape output) or have individual classes suppressed.

Also fixes a bug uncovered while wiring this into Bonsai: ready(),
where geometry_settings() actually gets read into the serializer,
was only ever invoked explicitly by IfcConvert's CLI driver and
isn't exposed to Python. Every Svg* setting -- including the three
from previous rounds -- silently stayed at its hardcoded constructor
default when the serializer was constructed directly through the
Python bindings, as Bonsai does. Fixed by calling ready() from
SvgSerializer's own constructor, safe since it only reads
geometry_settings() with no other side effects, and settings are
always finalized before construction in every call path.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:33:15 +01:00
Stephen Boddy 2ac92f01e4 Fix missing silhouette on curved analytic column/pile faces
Circular-profile IfcColumn/IfcPile elements produce a genuine
analytic cylindrical BRep face (via BRepPrimAPI_MakePrism), not a
tessellated facet. The edge classification/extraction pipeline is
edge-identity-based end to end, but a smooth surface's silhouette is
synthesized by HLR on the fly and has no corresponding pre-existing
edge to bucket, so it was silently dropped once any edge in the
product had been classified. Add a face-level pass that includes any
non-planar face directly in the outline bucket, giving HLR's
per-face OutLine reconstruction a face identity to correlate
against. Purely additive: diffing the whole test scene's output
before and after shows only the two previously-missing tangent
lines appear, nothing else changes.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:33:15 +01:00
Stephen Boddy 2e9e75c7bd Fix Issue 4: gate the back-facing crease flip by threshold
Re-enable the view-relative sign flip for folds seen through an
opening (e.g. a box with a face removed), reverted in the previous
commit after it corrupted unrelated geometry. The earlier revert's
diagnosis was slightly off: bucket reassignment can't affect HLR's
own visibility computation, so the corruption was actually an
asymmetric-threshold artifact -- an unconditional flip re-tested
small, correctly-flush deviations against the much smaller valley
threshold instead of the ridge one. Gating the flip so it only
reinterprets folds that already clear their own pre-flip threshold
fixes the box case while leaving every other test object's
classification unchanged (verified against the full test scene).

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:33:15 +01:00
Stephen Boddy 8857396a1f Fix SVG edge classification sign/threshold bugs
Fixes three bugs in classify_edge_from_faces() found via real-world
testing against a dedicated stress-test scene (icosphere, Suzanne,
cylinders/cones at various orientations, a dihedral-angle sweep rig):

- The outline (silhouette) test used a bare sign comparison, so a face
  at or near exactly edge-on to the camera could land on the wrong
  side of zero and fall through to angle-based classification instead
  of being drawn as outline. Now uses a tolerance band around zero,
  matching an equivalent check already used elsewhere in this file.
- The signed deviation-from-flat formula was inverted (180 - angle
  instead of angle), so small, genuinely near-flat facet angles came
  out with a large computed deviation and always classified as
  sharp/crease, never flush. This is why thresholds appeared to have
  no effect. Also replaced the edge/wire-orientation-based convexity
  sign (unreliable on real BRep topology, verified wrong against a
  known fully-convex icosphere) with a simpler position-based test.
- A specific edge that was previously missing entirely (not just
  misclassified) reappears correctly as a side effect of the outline
  fix above; no separate change was needed for it.

A fourth issue (folds viewed through an opening, e.g. a box missing a
face, should read as crease rather than sharp) was attempted via a
back-facing sign flip, but reverted: it broke the fixes above broadly,
since "both faces back-facing" isn't a rare look-through-a-hole case
once HLR has already filtered to visible edges only. Documented in a
code comment for whoever picks this up next.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:33:15 +01:00
Stephen Boddy f0970b90b0 Classify projection edges in SVG elevations
Adds boundary/outline/sharp/crease/flush classification of HLR
projection edges in SvgSerializer, so CSS can style silhouettes,
ridges, and valleys differently instead of drawing every edge
identically (fixes the "ugly faceted sphere" problem from #3668).

Classification happens pre-HLR on the original solid's real face
topology (three prior attempts tried to classify HLR's own output,
which carries no face topology at all and can't be correlated back
by edge identity). Each class's visible portion is then extracted via
HLRBRep_HLRToShape::VCompound(S)/OutLineVCompound(S), the same
per-shape filtering mechanism already used for per-product
segmentation, applied per class instead. Classes are tagged directly
on individual <path> elements so Bonsai's merge_linework_and_add_metadata
group-level class rewrite in operator.py never touches them.

New settings: svg-ridge-angle-min-degrees, svg-valley-angle-min-degrees,
svg-emit-flush-edges (ConversionSettings.h), wired through Bonsai's
CreateDrawing operator and exposed via its redo panel.

Refs #3668.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:33:15 +01:00
Andrej730 71a598e63a ifcopenshell.file: small wording fix 2026-07-17 21:55:22 +05:00
Andrej730 3d8115ebc5 ifcopenshell.file: drop workarounds for older builds
Introduced in aeed371 and it's been a while.
2026-07-17 21:55:22 +05:00
Ryan Schultz b5a0f1fc74 Bonsai: allow cross-family class reassignment for spatial elements with geometry (#8665)
The Reassign Class operator refused to reassign an element to a different
IFC product family unless it was an IfcElement <-> IfcElementType swap, so a
piece of geometry mistakenly hosted on IfcSite could not be turned into
IfcFurniture even though root.reassign_class handles it fine.

Loosen the guard: only block the case that actually matters - a spatial
element (IfcSpatialElement / IfcSpatialStructureElement for IFC2X3) with no
geometry, which would be a real containment-hierarchy container rather than
a stray modelled object. Everything else reassigns freely.

Closes #8664

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 18:57:11 -05:00
Petru Conduraru 25441bd816 Bonsai: make 'has openings' representation error actionable (#8108)
When converting a wall representation to a parametric extrusion via the
Representation Utilities buttons, an element that has openings would report
"has openings - representation cannot be updated" and stop, without telling
the user there is an ALT+click path that bakes the openings into the new
representation. Point the message at that path so the error is actionable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:35:46 +02:00
Petru Conduraru 65811ac7c9 Bonsai: place auto-generated opening boundaries at their real position (#8237) (#8311)
* Bonsai: place auto-generated opening boundaries at their real position #8237

auto_generate_boundaries (single-space mode) built each opening/filling boundary
from the opening's LOCAL geometry (get_vertices) but first did
mat.translation = (0, 0, 0) on its placement matrix. Because the vertices are
local, that placement translation is exactly what carries the opening to its
real location, so zeroing it collapsed every window/door boundary onto the
origin. This is why the auto path misplaced window boundaries while the
single-element path (create_element_boundary) placed them correctly, as
@MDHering observed with the two modes. Keep the full placement matrix.

Verified on the reporter's file: the opening's real placement is (0.1, 1.5, 1.0);
a vertex went from (0.6, 0, 0) under the old code to (0.7, 1.5, 1.0) with the fix,
i.e. moved by exactly the (0.1, 1.5, 1.0) that was being discarded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remove superfluous comment from #8237 fix

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: CyrilWaechter <cyril@biminsight.ch>
2026-07-16 20:17:02 +02:00
Andrej730 d9d1824886 test-package: drop stale comment
This information is already documented in maintanence.rst.
2026-07-16 19:03:35 +05:00
Andrej730 16e5f18553 Bump build 3e7b739 -> 821cf7b
Just to test everything is working with the changes from the last month.
2026-07-16 18:59:44 +05:00
Andrej730 b7a9b7bc5a test-package: assert BUILD_COMMIT is a 7-char short SHA 2026-07-16 18:56:44 +05:00
Andrej730 e14397058d test-package: verify build URLs with HEAD requests instead of scraping listing page 2026-07-16 18:53:54 +05:00
Andrej730 f0117c60b3 stub: sync added/removed symbols 2026-07-16 18:09:32 +05:00
Andrej730 0e5223a30d stub: add missing arrange_polygon_settings (158756e921) 2026-07-16 18:00:55 +05:00
Andrej730 bc41ff78f4 stub: drop abstract_arrangement (158756e921)
And also gnore delete_same_facet_edge_pairs as it's more of an interanl API.
2026-07-16 18:00:47 +05:00
Andrej730 9123d8c183 stub: add missing entity.inverse_attributes 2026-07-16 17:35:52 +05:00
Andrej730 ffd939508c ci-lint: run ty-bonsai and ty-ios as separate steps
So if one fails, it wouldn't block another.
Noticed by Stephen in d5e890bccd
2026-07-16 17:28:24 +05:00
Andrej730 9213b31235 logger: reuse logger_or_root, dedupe optional-logger-arg pattern 2026-07-16 17:28:24 +05:00
Andrej730 2155e3206f logger: use Logger* instead of Logger& to propagate signature using swig
See the comment in IfcLogger.h explaining this.
2026-07-16 17:28:24 +05:00
Andrej730 9e0c6cf524 util.schema: dedupe inline schema resolution logic 2026-07-16 17:28:23 +05:00
Andrej730 d5dc069b2f util.schema: fix geometry_classes_introduced_after using wrong IFC4X3 schema
It was passing `IFC4X3` directly to `schema_by_name` which is expecting
schema identifier (e.g. IFC4X3_ADD2, not IFC4X3 allowed by `IFC_SCHEMA`
- IFC4X3 is one of the IFC4X3 iterations while it was in development,
not the final one).

Noticed by tests failing:
FAILED
test/util/test_schema.py::TestGeometryClassesIntroducedAfter::test_ifc4x3_to_ifc2x3_is_superset_of_ifc4_to_ifc2x3
- RuntimeError: No schema named IFC4X3
FAILED
test/util/test_schema.py::TestGeometryClassesIntroducedAfter::test_ifc4_to_ifc4x3_is_empty
- RuntimeError: No schema named IFC4X3
2026-07-16 17:28:23 +05:00
Andrej730 821cf7b671 ifcparse: replace std::to_chars to fix mac build (ee2b357d7) 2026-07-16 11:57:37 +05:00
Andrej730 e01979187a build-all: fix issue building rocksdb on gcc 15
Example error: `error: ‘uint64_t’ has not been declared uint64_t blob_file_number, uint64_t total_blob_count,`

See https://github.com/facebook/rocksdb/issues/13365
2026-07-15 19:20:17 +05:00
Andrej730 816eba5145 build-all: ensure all patches are present
Also changed type to just `list[str]` to keep it simple.
2026-07-15 19:20:17 +05:00
Andrej730 4963bddd06 build-all: fix issue on gcc 15
Error was:
```
configure: error: could not find a working compiler, see config.log for details
```

config.log:
```
conftest.c: In function 'f':
conftest.c:12:48: error: too many arguments to function 'g'; expected 0, have 6
   12 | for(i=0;i<1;i++){if(e(got,got,9,d[i].n)==0)h();g(i,d[i].src,d[i].n,got,d[i].want,9);if(d[i].n)h();}}
      |                                                ^ ~
```
2026-07-15 19:20:17 +05:00
Andrej730 8718db63da pyproject: flip ty rules to error-by-default, review all new rules added since version bump 2026-07-15 19:20:17 +05:00
Andrej730 c013b9aca7 build-all: drop unused opencollada pr622 patch
Last reference to this file was dropped in 7ae685dbf, though the ref was
pointing to `/patches/opencollada/pr622.patch`, so IIUC
`patches/pr622.patch` was never used.
2026-07-15 19:20:17 +05:00
Andrej730 24e454ce0c build-all: drop unused occt patch
Introduced in e21277e80, reference removed
in 683cadeb7 when occt was bumped to 7.3.0 and switched to git-tag based
download.
2026-07-15 19:20:17 +05:00
Andrej730 cb497b37f7 pyproject: add nix script to ty check 2026-07-15 19:20:17 +05:00
Andrej730 71c6950a59 ifcclash: fix use of undefined clash["position"]
It's an artifact from the old hppfcl clasher dropped in 18c38b312
2026-07-15 19:20:17 +05:00
Andrej730 5a8aa0a659 bsdd: raise informative HTTPError
Previously we were just passing `.json()` which allowed too many request error slip in to later occur as missing attributes on the dictionaries.
2026-07-15 19:20:17 +05:00
Andrej730 f4526d152f bsdd: warn about include_class_properties deprecation
See https://github.com/buildingSMART/bSDD/issues/149
2026-07-15 19:20:17 +05:00
Andrej730 a7a7edfd27 bsdd: fix test_get_class_relations
`classRelations` doesn't exist on `ClassPropertiesContractV1`, probably was just a typo.
2026-07-15 19:20:17 +05:00
Andrej730 6ca8c8ac94 pyproject: add more packages to dev-setup 2026-07-15 19:20:17 +05:00
Andrej730 5273569b08 build-all: fix note about the schemas built by default 2026-07-15 19:20:17 +05:00
Andrej730 78712ead98 misc: more readable poll error for import_quick_favorites 2026-07-14 19:57:31 +05:00
Andrej730 6efb8a4373 misc: add Blender 5.2 offset for Quick Favorites user_menus 2026-07-14 19:57:31 +05:00
Andrej730 9e25c12b16 surveyor: drop never used dead code
Surveyor test was failing because `get_z_rotation` and `set_z_rotation` were not implemented.
The code was added in 230cbe1fd8, but it was never used.
2026-07-14 19:57:31 +05:00
Andrej730 0968d06780 Deduplicate code by reusing tool.document 2026-07-14 19:57:31 +05:00
Andrej730 3a8619726b core.drawing: deduplicate code, fix test
Core test was trying to access actual ifc data (`ifc.get().by_type("IfcGroup")` and was failing.
2026-07-14 19:57:31 +05:00
Andrej730 1b1da821f1 file.get_inverse: document with_attribute_indices overload 2026-07-14 19:29:34 +05:00
Andrej730 d772b24bd6 geometry.add_boolean: fix typo in the class name
🫣🫣
2026-07-14 19:29:34 +05:00
Andrej730 549f81a76e ios pyproject: add networkx stubs as dev dependency 2026-07-14 19:29:34 +05:00
Andrej730 e5c7206a37 express: fix use of non-existent ifcexpressparser
`express.bnf` arg wasn't handled since d506ad77b
`ifcexpressparser` waa moved inside `ifcopenshell-python` awhile ago too
2026-07-14 19:29:34 +05:00
Andrej730 5b968d5c75 pyparsing: fix using deprecated aliases
Deprecated since pyparsing 3.0 and produce runtime warnings. New function work exactly the same, except their name is pep8 compatible.
2026-07-14 19:29:34 +05:00
Andrej730 6075187720 express_parser: fix non-idempotent results 2026-07-14 18:42:12 +05:00
Andrej730 92528d84cd express: clean up trailing spaces 2026-07-14 18:42:12 +05:00
Andrej730 3955718145 express: update transpiled express rules using latest Python's AST
AST parser has changed a bit and there are some minor differences in the .py output. Updating files just to avoid seeing these diffs when rerunning rule compiler.
2026-07-14 18:42:12 +05:00
Andrej730 501246cd0b rule_compiler: fix error running on Python 3.14
Example error:
```
    ast.Str(s=node.attr),
    ^^^^^^^
AttributeError: module 'ast' has no attribute 'Str'
```

`ast.Str` was deprecated since 3.8 and was removed in 3.14, see https://docs.python.org/3/whatsnew/3.14.html#id9
2026-07-14 18:42:12 +05:00
Andrej730 1b863ff8be pyproject: add dev-setup poe task to setup environment for ide 2026-07-14 18:42:11 +05:00
Andrej730 dcd88b6cdd pyproject: Move tool deps from to requirements-tools.txt
Because uv was always trying to install when starting a venv in `ifcopenshell` folder, though they might be already available globally. And also they were listed twice - in pyproject and in the ci-lint.yml, now there's a single source of truth.
2026-07-14 18:42:05 +05:00
Andrej730 fb8b2ee878 bonsai pyproject: move pytest deps to requirements-dev.txt 2026-07-14 18:42:05 +05:00
Andrej730 8a00ce84cc geom/main.py: fix ty complaint 2026-07-14 14:56:06 +05:00
Andrej730 4032bbbd17 entity_instance.py: fix oveloads signatures (f93d79dc)
Without `/` overload implies that it also accepts kw args, while the implementation signature doesn't support them.
2026-07-14 14:56:06 +05:00
Andrej730 d728f09d86 Bump ty
Dropping `conflicting-argument-forms` rule as it was removed in ty 0.0.49.
2026-07-14 14:56:06 +05:00
Andrej730 3e9ef82448 maintenance.rst: move pyver matrix to bundled Python version section 2026-07-14 14:56:06 +05:00
Andrej730 a5c77fd096 ci-bonsai-daily: Use Blender 5.2 for tests 2026-07-14 14:56:06 +05:00
Andrej730 56ed79792e bonsai tests: fix test_failed_to_load_returns_only_base_keys (fdb2947) 2026-07-14 14:56:06 +05:00
Andrej730 9c91727402 express: drop Python 2 fallbacks 2026-07-14 14:56:06 +05:00
Andrej730 d183961280 pyproject: support formatting with ruff
Since it's black-compatible drop-in replacement and they can be used
almost interchangeably.
2026-07-14 14:56:06 +05:00
Andrej730 d30c25010c dev_environment.py: detect Python 3.13 on any Blender 5.1+ 2026-07-14 12:18:00 +05:00
Andrej730 97d1a6e488 dev_environment.py: add shebang and make executable 2026-07-14 12:12:54 +05:00
sboddy 6dec340161 Merge pull request #8576 from IfcOpenShell/fix/docker-macos-arm64-compat
docker: more robust in getting a GID, and editing the .env file.
2026-07-13 13:09:29 +01:00
Petru Conduraru 780739719f Bonsai docs: fix version switcher scheme mismatch (http vs https)
versionURLs in brand.html used http:// while the docs sites are
served over https://, so currentURL.includes(url) never matched and
the <select> never reflected/switched to Unstable. Fixes #8023.

Generated with the assistance of an AI coding tool.
2026-07-13 20:44:19 +10:00
Petru Conduraru 4a717ca7ff Fix ci-bonsai-daily: reconnect Cost/IfcGit tool interfaces (TestImplementsTool)
Two TestImplementsTool failures on v0.8.0:

- test_cost.py: Cost could not be instantiated because
  core.tool.Cost declared abstract get_direct_cost_item_products, which
  tool.cost.Cost never implements. The method is dead (zero call sites;
  get_cost_item_products(is_deep=False) already covers the 'direct'
  case), so remove the abstract declaration.
- test_ifcgit.py: tool.ifcgit.IfcGit was not declared as a subclass of
  its core.tool.IfcGit interface (unlike every sibling tool class), so
  the isinstance check failed. Add the base class (and the
  bonsai.core.tool import it needs). All 50 interface methods are
  already implemented on the concrete class.

No behaviour change. Verified in headless Blender: isinstance(Cost(), core.tool.Cost) and isinstance(IfcGit(), core.tool.IfcGit) both True (were TypeError / False); repo abstract-vs-impl diff confirms all IfcGit abstracts are implemented.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:43:25 +10:00
Petru Conduraru 5a831e3d21 Fix ci-lint: black-format selector.py
black (the version CI's psf/black@stable resolves to) flags three spots
in util/selector.py: the chained .replace() in FormatTransformer.number,
the suppress_zero_inches kwarg in format_length, and the long
`elif key in (...) and hasattr(...)` placement-key tuple in
set_element_value. Reformat all three to black's multi-line style.
Formatting only, no behavioural change (all keys preserved).

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:42:25 +10:00
Petru Conduraru d30286225c Bonsai: deterministic annotation order in generated drawing SVGs (#6608)
generate_annotation built the annotation list from a set union and sorted it by
ZIndex and TEXT-ness only. Annotations that tied on that key kept set iteration
order, which follows entity hash (step id plus the process memory address), so
the order of tied annotations (for example a label and its background fill)
shuffled between Blender restarts and flipped their draw order.

Add the stable IFC step id as a final tiebreaker so the order is total and
session independent. Behavior preserving, no z-layer semantics changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 20:41:41 +10:00
Bruno Postle 65695fb878 ifcedit: fix Optional[entity_instance] coercion crash on native JSON values
coerce_value assumed value_str was always a CLI string, but ifcmcp
passes JSON-decoded native types (int, None) straight through. Guard
the Union/Optional "none" check so it only calls .lower() on strings,
and handle native None explicitly.
2026-07-13 08:55:43 +01:00
Bruno Postle ab15750747 ifcedit: include IfcSpace in default QTO element scope
IfcSpace is not a subtype of IfcElement, so quantify.run_quantify()'s
default selector silently skipped all spaces, reporting
elements_quantified: 0 with no error or warning.

Generated with the assistance of an AI coding tool.
2026-07-13 09:45:25 +01:00
Petru Conduraru 694a44e638 ifc4d: tolerate activities without a CalendarObjectId in P6 import (#5617)
Importing a Primavera P6 XML crashed with
`AttributeError: 'NoneType' object has no attribute 'text'` in
P62Ifc.parse_activity_xml, which read
activity.find("pr:CalendarObjectId").text unconditionally. CalendarObjectId
is optional on a P6 Activity; when omitted, the activity inherits the
project's ActivityDefaultCalendarObjectId.

Capture the project default in parse_xml and fall back to it when an
activity has no CalendarObjectId (`calendar_id or self.default_calendar_id`).

Verified on the reporter's attached file (20241021 Cronograma.xml): 3 of 14
activities lack a CalendarObjectId and reproduced the exact crash on
v0.8.0; after the fix parse_xml completes and those activities resolve to
the project default calendar "2" (a valid calendar in the file). An
activity with an explicit CalendarObjectId keeps its own value.

Fixes the P6 re-import crash reported in #5617 (that issue tracks several
Gantt items; this addresses the import AttributeError).

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 17:15:36 +10:00
Petru Conduraru a3950ac191 util.element: read property sets inside an IfcPropertySetDefinitionSet (#6330)
get_pset and get_psets assumed RelatingPropertyDefinition is a single property
definition and read definition.Name directly. When it is an
IfcPropertySetDefinitionSet (a defined type wrapping a list of property set
definitions) that attribute access raised AttributeError, so an element whose
psets are grouped in a set returned none of them.

Unpack IfcPropertySetDefinitionSet into its members in both loops and process
each one. Single property definitions and the psets_only and qtos_only filters
are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 17:06:18 +10:00
Petru Conduraru 6f90badda8 Fix ci-bonsai-daily: renumber stale STEP ids in BDD feature fixtures
Several BDD scenarios hardcode absolute representation-item object names
whose trailing number is the IFC STEP line id
(f"Item/{item.is_a()}/{item.id()}"). Those ids drift when file-creation
order changes; a recent shift moved all of them by a uniform -4, so the
scenarios failed with "Item/.../NN does not exist".

The failing step (the_object_name_exists in test_feature.py) dumps the
full bpy.data.objects listing on failure, so the correct current ids are
recoverable directly from the CI log (run 29208793599, tested commit
36e21e882f, an ancestor of HEAD with only a .gitignore commit between).
Renumber to match:
  IfcExtrudedAreaSolid/77->73, IfcPolygonalFaceSet/76->72,
  IfcVertexPoint/69->65, IfcEdge/72->68, IfcFace/74->70.

Verified against the CI failure dump (a local build produces different
ids, so this is validated by CI's own object listing rather than a local
run). boolean.feature also hardcodes IfcHalfSpaceSolid/90 and panel text
[91] downstream of the failing assertion, which CI never reached and so
never dumped; left as-is to avoid guessing - they will print a fresh dump
next run for a follow-up if still stale.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:49:33 +10:00
Petru Conduraru 8b05510d6c docker: fix GID collision and macOS sed portability
Two host-environment bugs in the build-env scripts that break on
macOS/Apple Silicon hosts, independent of target architecture:

- Dockerfile: groupadd fails outright when USER_GID collides with an
  existing system group in the rockylinux9 base image (e.g. macOS
  default user GID 20 "staff" collides with RHEL's GID 20 "games").
  Guard with getent so useradd attaches to the existing group instead.
- ifcos_env: `sed -si` is GNU-only syntax and errors under BSD/macOS
  sed. Do the UNIQUE_ID substitution via a portable temp-file + mv.

Per sboddy's review on the original PR: dropped the linux/amd64
platform-pin additions from this change. The stack already targets
Rocky9/x64 build outputs by design, and Docker Desktop on macOS has
no native container runtime regardless (it's a Linux VM either way),
so forcing the image to run under emulation doesn't produce anything
that's actually loadable into a native macOS Blender/Bonsai install.
That's a separate, harder problem worth solving via a native build
path instead (mirroring build_osx.yml), not by fighting emulation
here. These two fixes stand on their own merits on any host.

This change was made with the assistance of an AI tool.
2026-07-13 09:43:57 +03:00
Stephen Boddy b1470223d3 Share ccache volume across checkouts, cap at 2G
The ccache named volume had no explicit name, so Docker Compose
namespaced it under the per-checkout project name (derived from
UNIQUE_ID), giving each checkout its own cache even though
docker/README.md already documented them as shared. Give the volume
a fixed name so all checkouts attach the same one.

Measured cache size after a full build (IfcParse+IfcGeom+IfcConvert+
wrapper, one Python version) is ~300MB, only ~5% of the previous 5G
cap. Shrink CCACHE_MAXSIZE to 2G, which comfortably covers the shared
baseline plus per-branch deltas from several diverging checkouts.

Generated with the assistance of an AI coding tool.
2026-07-13 06:42:05 +01:00
Petru Conduraru f25b072fa0 docker: make the build env work on macOS / Apple Silicon hosts
Three host-portability fixes to the docker/ toolchain from #8564 so it
runs on macOS as well as Linux. All three are no-ops on native amd64
Linux.

1. Dockerfile: only groupadd when the target GID is free. macOS's default
   primary group `staff` is GID 20, which already exists as `games` in
   rockylinux:9, so `groupadd -g 20` aborted the image build. Guard with
   `getent group "${USER_GID}" || groupadd ...`; useradd -g accepts the
   existing GID.

2. ifcos_env unique(): replace GNU-only `sed -si` (BSD/macOS sed errors
   "illegal option -- s") with a portable `sed > tmp && mv` rewrite of the
   UNIQUE_ID line. Verified against macOS BSD sed.

3. create() + compose.yaml: build with an explicit `--platform linux/amd64`
   so the locally built image's platform matches the `platform:
   linux/amd64` pin in compose.yaml. Without it, on arm64 the local image
   is tagged linux/arm64, compose treats the platform-mismatched image as
   absent and tries to pull `ifcopenshell-build-env:updated` from Docker
   Hub (which does not exist -> access denied). Also add `pull_policy:
   never` as a safety net so a future mismatch surfaces as a clear "image
   not found" rather than a registry auth error.

Note: on Apple Silicon the amd64 build runs under emulation and a cold
full build is slow; ccache makes incremental rebuilds tolerable. A native
Linux/Intel host or CI remains the better choice for routine use, but these
fixes turn "hard broken" into "works with a caveat" on macOS.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 06:44:55 +03:00
sboddy ffb867f254 Add .gitignore entries for docker build env (#8569) 2026-07-12 22:23:42 +01:00
sboddy 36e21e882f Merge pull request #8568 from sboddy/fix-autosave-recovery-segfault
Fix segfault on autosave recovery dialog Cancel
2026-07-12 22:00:06 +01:00
Stephen Boddy d0eca6fa90 Fix segfault closing autosave recovery dialog
Reported: Blender segfaults when clicking Cancel on the "newer
autosave found" recovery popup shown by LoadProject at startup.

Root cause: LoadProject.execute()/invoke() triggered the recovery
popup via bpy.ops.bim.load_autosaved_recovery_popup("INVOKE_DEFAULT",
...) and returned that call's result ({'RUNNING_MODAL'}) as their own
return value, without LoadProject itself ever calling
modal_handler_add(). Blender's window manager takes a RUNNING_MODAL
return as a promise the operator registered its own modal handler;
since it hadn't, the WM's operator bookkeeping was left corrupted -
silently, since this is heap/state corruption rather than an
immediate crash. It only surfaced later, when the real modal operator
(the popup) closed and the WM reconciled its modal stack, which lines
up with the crash occurring specifically on dialog close regardless
of which button was pressed. check_autosave_recovery() now returns a
plain bool and fires the popup fire-and-forget; LoadProject reports
its own honest {"FINISHED"}.

Also hardened, as defense in depth: LoadAutosavedRecoveryPopup's
execute()/cancel() call back into bim.load_project(...), which (with
should_start_fresh_session) calls wm.read_homefile() and tears down
the window manager/screens. Doing that synchronously from inside this
popup's own execute()/cancel() - itself invoked from deep inside
Blender's modal handling for the popup's button click - risks the
same class of use-after-free as the timer bug fixed in the previous
commit. The reload is now deferred by one timer tick so it runs after
the popup's modal handling has fully unwound, and the deferred
callback closes over plain values rather than `self`, since the
operator instance may not survive past cancel()/execute() returning.

This defer-only change was tried and tested first, on the (incorrect)
assumption it was the root cause: it produced a byte-for-byte
identical crash backtrace on retest, which is what pointed at the
RUNNING_MODAL bug above as the actual cause - the defer change alone
was insufficient because the corruption happens when the popup is
first shown, not when it's closed.

Generated with the assistance of an AI coding tool.
2026-07-12 21:52:09 +01:00
Stephen Boddy 6306ce0f80 Fix autosave timer self-unregister crash risk
The periodic autosave timer called reset_timer() at the end of its
own callback, which unregistered the timer that was still executing
(itself). Blender frees the timer's internal registry entry on that
manual unregister, then frees it again when the callback returns
None - a double free that corrupts the heap and can crash Blender
later, once the corrupted memory is reused.

Reschedule by returning the next interval from the callback instead,
which is the safe, documented way to repeat a bpy.app.timers
callback. External reset_timer() calls (from SaveProject,
LoadProject, AutosavePrompt) are unaffected since they run from a
separate call stack (UI events), not from inside the timer.

Found while investigating a segfault reported when cancelling the
autosave recovery popup; not itself the cause of that crash (see the
following commit), but the same reentrant-unregister pattern and a
real, independent latent bug in the periodic reminder path.

Generated with the assistance of an AI coding tool.
2026-07-12 21:51:56 +01:00
sboddy 53187ddae9 Merge pull request #8564 from sboddy/docker-build-env-tooling
Add a local docker build environment for IfcOpenShell (docker/)

See PR #8564 for full explanation.
2026-07-12 20:50:53 +01:00
Stephen Boddy 92c50ed3b4 Harden docker build tooling: non-root, clean lifecycle, try()
Dockerfile (renamed from Dockerfile_init, Dockerfile_update removed):
- Run as a non-root `builder` user matching the host UID/GID (passed as
  --build-arg by create() from id -u/id -g), so build output under the
  bind mount stays owned by the host user instead of root.
- Fix CCACHE_MAXSIZE: `ccache -M 5G` wrote its limit to a config file
  under /ccache at image-build time, but /ccache is a volume mount
  point, so that file gets shadowed by the (empty) volume the moment
  the container actually runs - the cap never took effect. Set
  CCACHE_MAXSIZE=5G as an image ENV instead.
- Dedupe ccache/libffi-devel, add --setopt=install_weak_deps=False
  --setopt=tsflags=nodocs, add `git lfs install --system`, combine the
  dnf update+install into one layer.
- Drop Dockerfile_update: it built FROM its own previous output, so
  every `update` call made the image strictly larger forever (Docker
  layers are append-only, `dnf clean` in a later layer can't shrink an
  earlier one). `update` now just calls create(), which already runs
  `dnf update -y` FROM a clean rockylinux:9 every time.

compose.yaml: pin platform: linux/amd64 so this doesn't silently run
under emulation on an ARM host.

ifcos_env:
- Split the previously-conflated stop/down into six distinct,
  Compose-native lifecycle commands: up (create-or-start), down
  (remove), stop, start, restart (stop+start, same container),
  recreate (down+up, fresh container). Previously `stop` was aliased
  to `down`, which silently removed the container instead of pausing
  it.
- Implement try(): copies the built wrapper into a real Blender/Bonsai
  install for manual testing, reading the target from a new
  BLENDER_USER_RESOURCE .env variable and auto-detecting the built
  Python version (disambiguating via PY_TGT for multi-version builds).
  Deliberately kept human-only - it mutates a live Blender install, so
  it shouldn't run unattended as part of an automated/AI workflow,
  which should instead copy the wrapper into the repo's own
  src/ifcopenshell-python/ifcopenshell/ (documented in SKILL.md).
- Fix unique(): the "has .env already got a UNIQUE_ID line" check
  referenced an unset $FILE instead of $ENV_FILE, so it always
  evaluated true and appended a fresh "UNIQUE_ID=dummy" line to .env
  on every single `up`.
- Minor: differentiate remove()'s log message from down()'s (no longer
  identical now that they're distinct operations), tidy help text
  alignment and a stray double-space typo in clean().

SKILL.md: rewritten as current-state documentation (no more "fixed in
this copy" changelog framing) covering the above, plus a migration
note for anyone hitting root-owned leftovers from an older image.

Verified by actually building the image and driving every new
lifecycle command (stop/start/restart keep the same container ID;
down+up and recreate produce a new one) and try() (including the
quoted-tilde BLENDER_USER_RESOURCE edge case) against the real container.

Generated with the assistance of an AI coding tool.
2026-07-12 20:35:01 +01:00
Stephen Boddy fa98aad469 First docker build environment
First functional version, but it needs some improvements and fixes
identified as I've used it personally on one thing, and when an AI
(Claude) used it to work through the CI test errors.

I had the AI make a SKILL.md file. If the AI indicates it needs to
build the ifcopenshell binary, use this and let it rip.
2026-07-12 18:43:27 +01:00
Petru Conduraru 980988f208 Bonsai: fix KeyError in format_distance for kilometre and mile units #8255
The project-unit to Blender-unit mapping in format_distance only knew
FOOT/INCH/METRE/DECIMETRE/CENTIMETRE/MILLIMETRE, so creating a project
with Kilometers or Miles in the New Project Wizard crashed with
KeyError: 'KILOMETRE' (or 'MILE') as soon as the spatial tree formatted
an elevation. Add the missing Blender-supported units (kilometre, mile,
micrometre) and fall through gracefully for anything else (for example
HECTOMETRE) so unknown units use the adaptive formatting branch instead
of raising.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:51:37 +10:00
Petru Conduraru d4805387ef Selector: add rotation_x/y/z value keys #6262
Expose the Euler rotation of an element's placement in degrees through
get_element_value, alongside the existing x/y/z and easting/northing/
elevation keys. This makes element rotation exportable through ifccsv,
e.g. for placing oriented symbols in GIS.

Adopts the approach agreed in the review of the stale PR #6272 by
@TZwielehner: reuse util.shape_builder.np_matrix_to_euler and do the
degree conversion inside get_element_value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:49:43 +10:00
Petru Conduraru 69a4be68e8 Bonsai: fall back to adaptive units for unsupported SI prefixes #8074
Project loading set scene length_unit to f"{Prefix}METERS", but Blender's
enum only defines KILOMETERS, CENTIMETERS, MILLIMETERS and MICROMETERS.
A model with a DECIMETRE (or HECTO/DECA/etc.) length unit therefore raised
on the enum assignment and the file failed to open. Guard with the set of
supported values and fall back to ADAPTIVE display for the rest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:38:10 +10:00
Petru Conduraru 06da416b8f docs: remove TODO placeholder sections from the create-model quickstart #8208
The quickstart ended with three empty sections whose bodies were only
"TODO" (placing occurrences, changing locations, modeling a building),
which read as a dead end on docs.bonsaibim.org. The page now ends on the
completed save-and-view flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:37:34 +10:00
Petru Conduraru 21ae78fbc2 resource.assign_resource: fix typo in duplicate guard #8203
The guard that avoids re-assigning the same object to the same resource
tested is_a("IfclRelAssignsToResource") (stray "l"), so it never matched.
A repeat assignment therefore fell through and appended the related object
to RelatedObjects a second time. Corrected to "IfcRelAssignsToResource".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:36:26 +10:00
Petru Conduraru 0a8ae14789 fix(ifcdiff): check attributes by default so PredefinedType changes are caught (#8214)
IfcDiff defaulted to relationships=["geometry"], so a plain diff only ever
compared geometry. Attribute-only edits on an element that kept its GlobalId
(a modified or removed PredefinedType, a renamed element, etc.) were silently
missed. The CLI made this worse: --relationships did not list "attributes" or
"geometry" as valid values, so there was no documented way to enable it.

The default is now ["attributes", "geometry"], so a plain `ifcdiff old new`
reports attribute changes alongside geometry changes. The CLI help and the
IfcDiff docstring now document all valid relationship values.

Added a regression test covering a PredefinedType change detected with the
default configuration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-12 22:29:31 +10:00
Petru Conduraru 2eea7728d2 fix(selector): round() should not crash on non-numeric values (#6776)
FormatTransformer.round() called Decimal() directly on the input value,
which raises decimal.InvalidOperation when the value is a non-numeric
string (a text property, or a value carrying a unit suffix like "12.5 m").
In a spreadsheet export this crashed the entire operation as soon as one
element carried such a value.

Now round() catches InvalidOperation and returns the value unchanged, the
same graceful-fallback convention used by add(). Numeric rounding is
unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-12 22:28:38 +10:00
Petru Conduraru 6b3cc54afc ifcfm: convert COBie Coordinate space points to project units (#5926)
In the cobie24 Coordinate sheet, Floor rows use get_local_placement, whose values
are in the project length unit, but Space rows come from ifcopenshell.geom
create_shape, whose vertices are in SI metres, and the space branch never scaled
them back. So on a non metre model (for example millimetres) the Coordinate sheet
mixed units a thousandfold apart and disagreed with the Facility sheet's declared
LinearUnits.

Scale the space bounding box by the project unit scale so the whole Coordinate
sheet is consistent. A metre model is unchanged since the scale is 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:27:07 +10:00
Petru Conduraru 5c11946470 Support block comments in selector filter syntax (#5023)
The filter_elements selector grammar had no way to comment out part of a
query, so users had to delete and retype text to temporarily toggle a
facet. Add a /* ... */ block comment terminal that is ignored by the
lexer, and tolerate a trailing "+" so that commenting out the final
operand (e.g. "IfcWall + /* IfcSlab */") parses cleanly. Comments may
span multiple lines; a /* sequence inside a quoted string is not treated
as a comment. Only the filter grammar is affected, not get_element or
format which use "/" for regex and division.

Adds a regression test and documents the syntax.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:23:30 +10:00
Ryan Schultz 0b7e25a3ef Docs: clarify immediate vs. any-depth spatial selectors
The location and parent filters both match at any depth in the spatial
hierarchy, which surprises users who want only the elements immediately
under a given container. Document that the parent query key resolves the
direct parent only (e.g. query:"parent.Name"="My Site"), add a matching
filter example, and note the immediacy on the parent value key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 16:44:22 -05:00
Ryan Schultz d16c283aef Add bulk-load of selected drawings' annotations (#8525)
SHIFT+CTRL+CLICK on Activate Drawing now imports the
annotations of all selected drawings without switching
the active view or camera, then selects their cameras with
the first as active. SHIFT+CTRL+ALT+CLICK also selects the
loaded annotation objects. The drawing camera is imported
when missing so annotations land in the correct collection.
Loading is idempotent.

Generated with the assistance of an AI coding tool.
2026-07-11 15:54:20 -05:00
Petru Conduraru a0f493b471 IfcConvert: report an error when the output file cannot be opened (#438)
Converting to a path whose directory does not exist (or is not writable)
failed silently: the serializer's ready() check correctly returned false,
but IfcConvert deleted the temp file and returned EXIT_FAILURE without any
message, so the user saw no reason for the failure.

Log a SYS error naming the output file before returning, matching the
existing "Unable to open output file" reporting used elsewhere.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:28:51 +02:00
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 7e3d2f936d build: do not request the header-only Boost.System component (build against Boost 1.70+)
Boost.System has been header-only since Boost 1.69 and its compiled stub
library was removed in newer Boost, so listing system in the requested
find_package components makes configuration fail on Boost 1.70 and up (for
example Boost 1.90 errors with "Could not find boost_system"). Boost.System
is still pulled in transitively by thread / iostreams where it is needed, so
drop it from the explicit component list.

Verified: with this change IfcOpenShell configures and builds IfcConvert
cleanly against Homebrew Boost 1.90 and OpenCASCADE 7.9.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:16:35 +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 b9deb9c63d Git ignores CLAUDE.local.md file
This allows a file that will be automatically picked up by Claude. It can either
be a copy of a CLAUDE.md, or a one line file pointing to a shared common file. i.e.

@~/.claude/conventions-ifcopenshell.md
2026-07-11 13:10:14 +01:00
sboddy e14b3ec8a0 Merge pull request #8243 from sboddy/feature-5753-autosave
Feature #5753 - Autosave for ifc files

Merging because it could be a life saver. It is hidden behind an option and is off by default.

- Provides the option have an autosave file created periodically (duration in prefs).
- Can be set to save immediately or a dialog prompt to save, but can be dismissed.
- Removes the autosave when Blender quits cleanly.
- If the autosave file exists at startup, it will prompt which file to load.

_Every_ AI had a hand in this, but I have reviewed, understood and tested it. AI Credits go to:
Cursor, Grok, Copilot, and Claude.
2026-07-11 11:10:59 +01: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
sboddy 256d5a63f1 Merge pull request #8495 from sboddy/lint-pass
Fix ci-lint failures: black formatting, ruff unused imports, ty type errors
2026-07-10 22:47:46 +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 402591e71c ci: restore --shared on the Rocky builds (undo datamodel-merge regression)
The datamodel-v1.0 merge (cf05bbd1b) overwrote build_rocky.yml with a
version that switched python3 -> uv run but dropped the --shared flag that
a91b1da28 ("Reduce Rocky package size") had added. build_osx.yml kept it
(ddee88bed).

Without --shared, nix/build-all.py builds IfcOpenShell as static libs, so
each of the ~40 plug-in .so files (schemas x8, kernels, mappings,
serializers, writers) statically embeds a full copy of libIfcParse +
libIfcGeom. The data-model rewrite made those base libs much larger, so the
duplication ballooned the Linux packages (~2-3x). With --shared the plug-ins
dynamically reference the shared libIfcParse/libIfcGeom instead. Restores the
same size reduction macOS already has.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 12:53:32 +10: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 21ba06cd94 ci: install FLTK's X11/pango/cairo + static libstdc++ for the connector link
With dbus-devel added, the connector's Rust code compiles fully and reaches
the final link, which fails: the bundled FLTK GUI toolkit needs the X11
extension, pango and cairo shared libs, plus libsupc++.a. ld reports every
unresolved -l at once, so this is the complete set:
  -lXext -lXinerama -lXcursor -lXrender -lXfixes -lXft
  -lpango-1.0 -lpangoxft-1.0 -lpangocairo-1.0 -lcairo -lsupc++
The X/pango/cairo -devel packages are in AppStream; libstdc++-static
(libsupc++.a) is in CRB, so enable it for the transaction. GitHub's ubuntu
runners ship all of this, which is why the dedicated connector workflow
never needed it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:49:46 +10:00
Dion Moult 0908a5b5a3 ci: install dbus-devel for the Rust connector's keyring backend
cargo build of bonsaiviewer-autodesk pulls dbus-secret-service (the Linux
OS-keyring backend for credential storage) -> dbus -> libdbus-sys, whose
build.rs needs dbus-1.pc via pkg-config. GitHub's ubuntu runners ship
libdbus-1-dev, so the dedicated connector workflow never needed it; the
minimal Rocky container doesn't. Add dbus-devel to both Rocky jobs
(pkg-config is already present). This was the last step after a fully
successful C++ build + cargo compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:31:46 +10:00
Dion Moult b72daf1330 ci(arm): set PATH in the Rocky 10 container so run steps find sh
The community rockylinux/rockylinux:10 image omits PATH from its image
config, unlike the old Docker Official arm64v8/rockylinux:9. GitHub Actions
derives each run step's PATH from that config, so with no PATH the shell
exec (docker exec ... sh -e {0}) fails with exit 127, 'exec: sh: not found'
— it broke before any build logic ran. Restore a standard PATH via the
container env; the runner still layers GITHUB_PATH additions (uv, cargo)
on top.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:21:42 +10:00
Dion Moult 014baab708 ci: provision Rocky jobs to build+package BonsaiViewer & Rust connector
Two gaps left when BonsaiViewer and its Rust connector were newly added to
the Rocky CI jobs (May–Jun), neither previously exercised there:

1. Rust: the autodesk connector was rewritten from a PyInstaller Python
   app to a Rust crate, so packaging/build.py now runs 'cargo build
   --release'. Neither Rocky workflow installed a toolchain. Add rustup
   (stable, matching the dedicated dtolnay/rust-toolchain@stable workflow)
   to both x86 and ARM.

2. ARM glibc: aqt's official Qt6 ARM binaries link glibc 2.38, which Rocky
   9 (glibc 2.34) can't load — moc fails, breaking IfcViewer_autogen. Move
   the ARM job to Rocky 10 (glibc 2.39). The legacy arm64v8/rockylinux
   image stopped at 9, so use rockylinux/rockylinux:10 (multi-arch, has
   arm64). Rocky 10 defaults to Python 3.12 and drops python3.11, so the
   script-runner references move python3.11 -> python3 (system Python only
   runs helper scripts; ifcopenshell is built against uv's Python). Bump
   the ccache key to rockylinux10. x86 stays on Rocky 9 to keep its lower
   glibc floor for end users.

The rockylinux9-arm64 build-outputs deps branch is kept as-is: Rocky 9
deps are forward-compatible on Rocky 10, and no rocky10 branch exists yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:07:02 +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
Dion Moult ecee648b44 ci: install aqtinstall into the uv run env (fix Linux Qt6 install)
build-all.py's install_qt6 runs `sys.executable -m aqt`, but the build now
runs under `uv run`, whose isolated env never got aqtinstall — it was pip
installed into the system Python. `uv run --with typing_extensions --with
aqtinstall` puts them where the script actually executes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 21:51:49 +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 9ae79b42dd Merge pull request #8398 from Gorgious56/batch-array-duplicate-helper
Batch array duplicate helper
2026-07-08 15:27:53 +02: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
Gorgious56 c299f0b191 Bonsai: iterate every duplicated entity in relationship recreation
Sweep [0]-indexing in recreate_aggregate, recreate_connections,
and recreate_port_connections so batched N-child duplicates
recreate relationships on every new child, not just the first.
Single-source callers unaffected (loop collapses to one iteration
on 1-element lists).

Relates to #8088.

Generated with the assistance of an AI coding tool.
2026-07-08 13:19:26 +02:00
Gorgious56 8f3a1d7412 Bonsai: batch array-duplicate + defensive guards
Replace N sequential duplicate_ifc_objects([parent]) calls in
_regenerate_array_body with one duplicate_ifc_object_n_times call
per layer, batching the fixed per-call overhead (snapshot gather,
UI refresh, decorator reload).

Guard batch_host_recut drain against dead StructRNA refs and prune
orphan array-child GUIDs at regen so outliner-delete of a
Bonsai-managed child cannot crash subsequent regenerate_array.

Recalculate walls after recreate_connections so Shift+D of
connected walls produces correct junction geometry without a
manual regen step.

Relates to #8088.

Generated with the assistance of an AI coding tool.
2026-07-08 13:13:14 +02:00
Dion Moult 392af501d1 properties: real empty states + smaller base UI font
- Replace the mock IfcWall placeholder with a "No item selected" empty
  state; the panel only fills in class/attributes/relationships/psets from
  a resolved object, and safely stays empty otherwise.
- Show "No properties" / "No quantities" placeholders (muted, themed via
  secondary_text) when those sets are empty.
- Drop the base application font 10pt -> 9pt to fit more data. Panel titles
  keep their own explicit size and are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:37:17 +10:00
Dion Moult 4afb3892a2 spatial hierarchy: storey elevation + Long Name column + resizable layout
- helpers/placement: port get_storey_elevation (placement Z, falling back
  to the Elevation attribute), matching ifcopenshell.util.placement.
- Add a secondary column: the storey elevation for IfcBuildingStorey,
  otherwise the LongName when filled. Elevations are right-aligned.
- Columns: Name is drag-resizable (interactive) and defaults to 20% of the
  width, Long Name stretches to fill the rest, and the eye is pinned to the
  right at a fixed width. Header shown so the divider can be grabbed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:33:29 +10:00
Dion Moult 90196dd51d viewport: idle the render loop when only unfetchable chunks remain
The streaming settle burst re-armed the render loop whenever a
non-resident chunk was frustum-visible, but the enqueue only fetches
chunks that are contribution-visible (big enough on screen) and not in a
blocked cooldown. A chunk that is in the frustum but sub-pixel is never
loaded, so visible_pending stayed true forever and the loop spun at full
frame rate with no input.

Match visible_pending to the enqueue's eligibility test: a non-resident
chunk keeps the loop alive only if it's actively loading, or is
contribution-visible and past its cooldown. Sub-pixel / cooldown-blocked
chunks no longer prevent idle; they still stream in when a camera move or
eviction requests a frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 19:43:07 +10:00
Petru Conduraru e0a1988044 Follow IfcRelAdheresToElement so IfcSurfaceFeature road markings import #8375
IfcSurfaceFeature (e.g. road markings) adheres to a host element through
IfcRelAdheresToElement, a [1:1] cardinality hierarchical relationship in the
same family as aggregation, containment and nesting since IFC4.3. The spatial
traversal never followed it, so surface features had no resolvable parent or
container: on import they landed in the Unsorted collection instead of the
host's spatial collection, and were dropped entirely in DECOMPOSITION filter
mode.

Add get_adhered_element (feature to host) to the get_parent resolver chain and
walk HasSurfaceFeatures in get_decomposition, plus a get_surface_features helper
mirroring get_parts/get_contained. With get_parent resolving adherence,
get_container now returns the host's spatial container, so tool.Collector places
surface features under the host. Also follow HasSurfaceFeatures in the Bonsai
DECOMPOSITION filter path so they load in that mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 11:16:08 +02:00
Dion Moult 60cab3e7c4 spatial hierarchy from IFC + active-model concept
Build the spatial hierarchy panel from the loaded model's real IFC
spatial structure instead of mock data:
- helpers/element: add get_spatial_children (IsDecomposedBy -> RelatedObjects,
  filtered to spatial elements) to walk IfcProject -> IfcSite -> IfcBuilding
  -> IfcBuildingStorey -> IfcSpace.
- SessionState: relay dataSourceReady as modelDataSourceReady (the .ifc for a
  sidecar hit loads asynchronously, so the tree can only build once it arrives).
- spatial_hierarchy/View: walk the active model's IFC file into a TreeNode
  tree, naming nodes by Name (fallback to class), mapping site/building/storey
  kinds; siblings sorted with natural (numeric) collation.
- spatial_hierarchy/Panel: tree now fills the panel height (setBodyExpanding +
  Expanding size policy); right-click menu for recursive Expand/Collapse
  Subtree and Expand/Collapse All.

Add the concept of an active model:
- SessionState: activeModelId / setActiveModelId / activeModelChanged; the
  first loaded model is active by default; reassigns/clears on removal.
- Models panel: clicking a model makes it active; its cube icon is drawn with
  the accent colour (makeAccentSvgIcon) via FederationItemModel::setActiveModelId.
- The spatial hierarchy reflects only the active model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:38:42 +10:00
Dion Moult ec285fc32c properties: show real property and quantity sets
Populate the Properties and Quantities sections from the pset helper:
get_psets(psets_only) for Pset_*, get_psets(qtos_only) for Qto_* /
BaseQuantities, inheriting occurrence-over-type values. A toPropertySets
converter drops the internal "id" key and non-scalar values, formats
scalars for single-line cells, and skips empty sets. Placeholders are
cleared once a project is loaded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:22:05 +10:00
Dion Moult 8ac7b4373e properties: real attributes + relationships; keep selection on deselect
Add helpers to src/helpers/element: get_scalar_attributes (primitive
EXPRESS attributes only — entity refs / aggregates omitted), get_type
and get_container (ports of ifcopenshell.util.element), and a public
get_string_attribute for safe by-name reads.

Wire them into the properties panel:
- Attributes section shows the element's direct primitive attributes for
  live entities, or cached GlobalId / Name for geometry-only elements.
- Relationships section shows the construction Type and spatial Container
  by name (falling back to the class when unnamed).
- Placeholders are cleared once a project is loaded, so no mock data leaks.

Also: a deselect (click on empty space -> object_id 0) no longer resets
the panel; it keeps showing the last active object. Project reset/open
still clear it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:14:13 +10:00
Dion Moult c5ea612110 helpers: port util.element.get_predefined_type; show it in properties
Add src/helpers/element.{h,cpp}, a schema-dispatched C++ port of
ifcopenshell.util.element.get_predefined_type: prefers the associated
type element's predefined type (IsTypedBy / IsDefinedBy), falls back to
ElementType / ProcessType when USERDEFINED, then the occurrence's own
PredefinedType / ObjectType. Attribute reads are by-name so they work
across the IfcElement / IfcType* subtypes that carry these attributes.

Wire it into the properties panel entity summary: live IFC entities show
their real predefined type; geometry-only elements (a .ifcview loaded
without its .ifc/.rdb) show "N/A". Clears the placeholder so a stale
predefined type no longer leaks once a project is loaded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:48:09 +10:00
Dion Moult 75c9da5098 ifcviewer: overhaul model/object ID tracking
Rename the two overloaded model identifiers and make object_id
assignment single-authority, fixing a pick -> properties mismatch.

Identifiers:
- Per-model UUID fed_id -> model_id; the uint32 runtime handle
  model_id -> session_model_id (SessionState accessors + mirror hashes
  renamed to match). "fed_id" was a misnomer -- the federation is the
  whole collection, not one model.

object_id assignment (fixes wrong class on click):
- Producers (GeometryStreamer, .ifcview sidecar) now stamp model-LOCAL
  object_ids; ViewportCore::applyCachedModel is the sole authority that
  assigns the session-global id (base + local). Removed
  SceneLoader::next_object_id_, GeometryStreamer::lastObjectId(), and the
  streamer's start_object_id parameter.
- The element table is stamped by the same base on both load paths
  (applySidecarData and onStreamerFinished), so registry ids match the
  ids pick returns. Previously the sidecar path double-rebased instances
  vs the registry (click IfcSite -> showed IfcDoor); the live-stream path
  had the same latent mismatch. Both closed.

Naming / cleanup:
- SceneLoader::addFiles -> queueModels; startStreamLoadFor ->
  loadFromGeometryStreamer; readSidecarMetadataOnly -> readSidecarMetadata.
- Federation::addModel takes an explicit display_name (no QFileInfo
  fallback); callers pass QFileInfo(path).fileName().
- Disambiguate cryptic short locals (d->sidecar, m->model, c->chunk, ...)
  in SceneLoader, Federation, ViewportWindow, AreaMeasurement,
  SectionGizmoRenderer, and the SidecarData/SidecarReadPlan spots in
  ViewportCore.

Tests: 125/125 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:10:05 +10:00
Richard Brice 644b92263d Merge branch 'v0.8.0' of https://github.com/IfcOpenShell/IfcOpenShell into v0.8.0 2026-07-07 12:00:27 -07:00
Richard Brice 61642d2ba3 Fixes computation of fallback position for linear placement. PlacementRelTo was improperly ignored 2026-07-07 11:59:47 -07:00
Petru Conduraru 4776bd7639 Atomic IFC file writes to prevent corruption on interrupted save (#4797)
file.write() streamed directly onto the target path, so a crash mid-write
left a truncated file with dangling STEP references. Serialize to a temp
file in the same directory, then atomically rename it onto the target.

- New IfcUtil::path::atomic_rename_file: std::rename on POSIX, MoveFileExW
  with MOVEFILE_REPLACE_EXISTING on Windows. Unlike rename_file it never
  unlinks the destination first, so there is no window where it goes missing.
- Fully in C++/swig (per aothms), so the FILE_NAME header is untouched: it
  comes from the model header, not the output path (verified empirically).
- Temp lives next to the target so the rename stays on one filesystem.
- Stream is closed before the rename (Windows cannot move an open file).
- On any write error the temp is removed and the original target is intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 14:22:44 +02:00
Thomas Krijnen 7864ae7814 New conversion settings 2026-07-07 10:53:28 +02:00
Thomas Krijnen 0a1b50cd46 Test scaffolds 2026-07-07 10:53:16 +02:00
Thomas Krijnen 08ebd05be5 Support vector<string> setting types 2026-07-07 10:13:54 +02:00
Petru Conduraru b2d58d0b81 cmake: read the VERSION file unconditionally so builds report the real version #8164
IfcConvert --version reported 0.8.0 on a plain source build even though the
VERSION file says 0.8.6 (#8164). buildinfo.cpp already falls back to the
IFCOPENSHELL_VERSION_STRING macro and CMake already passes it as
${RELEASE_VERSION}, but RELEASE_VERSION was only read from the VERSION file
when VERSION_OVERRIDE was on. A default build (VERSION_OVERRIDE off,
ADD_COMMIT_SHA off, as the nixpkgs package builds it) fell through to the
hardcoded "0.8.0", so the fallback macro carried the stale value.

Read the VERSION file unconditionally so RELEASE_VERSION is always the real
version. VERSION_OVERRIDE still governs the branch name embedded when
ADD_COMMIT_SHA is on, and project()/CPack now also reflect the true version.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 10:05:03 +02:00
Petru Conduraru 7322263a5e GltfSerializer: clamp roughnessFactor into the valid glTF range #8073
roughnessFactor was computed as 1/specularity. An IfcSpecularExponent of
0 produced infinity, which nlohmann::json serialises as null and makes
the glTF invalid; exponents below 1 produced values above 1, which glTF
also forbids. Map exponents <= 1 to full roughness and keep 1/exponent
above that, so the factor always lands in [0, 1].

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:33:47 +02:00
Petru Conduraru 49de7dbcb1 ExtractElements: handle IfcProject without RepresentationContexts #8199
The georeferencing fix (e6dc582) iterates IfcProject.RepresentationContexts
unconditionally, but the attribute is OPTIONAL and None on projects without
contexts, crashing every extraction on such files with
TypeError: 'NoneType' object is not iterable.

Also extend the #8199 regression test to assert element placements are
copied verbatim, so extraction can never bake map coordinates into local
placements.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:24:34 +02:00
Petru Conduraru bade0647e8 util.unit: scale RepresentationContext Precision on unit conversion #6127
IfcGeometricRepresentationContext.Precision is typed as a plain IfcReal
but is interpreted in the project length unit, so the IfcLengthMeasure
traversal in convert_file_length_units never touched it. A model
converted from mm to m kept a Precision of e.g. 0.01 (fine in mm, huge
in m), which breaks downstream geometry interpretation such as
IfcConvert boolean cleanup.

Subcontexts derive Precision from their parent, so only root contexts
are scaled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:21:25 +02:00
Petru Conduraru 58cfab48e6 entity_instance: get_info_2 falls back to get_info for unsupported args #4270
get_info_2 advertises the same signature as get_info but raised a bare
AssertionError for anything the C++ fast path does not implement --
including its own default arguments (recursive=False).

Use the fast path when recursive=True, return_type=dict and ignore=()
hold, and delegate to the pure Python get_info otherwise. As noted in
the issue, without recursion there is no meaningful performance gain to
lose by delegating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:15:32 +02:00
Petru Conduraru fa597536e1 IfcParse: drop ostringstream from format_double per review #7696
std::to_chars is locale-independent, so the ostringstream and imbue(locale)
are no longer needed. Build the REAL string with plain std::string operations.
Output is unchanged (verified in standalone compile: same shortest values, all
round-trip). Addresses review feedback on #8309.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:14:24 +02:00
Petru Conduraru ee2b357d74 IfcParse: serialize REALs with shortest round-trip form #7696
format_double formatted doubles with setprecision(max_digits10) (17 digits),
which padded clean values with noise: 0.0174532925199433 was rewritten as
0.017453292519943299 and 1.E-05 as 1.0000000000000001E-05. Every REAL in a file
changed on save, producing enormous diffs for anyone version-controlling IFC.
Use std::to_chars, which emits the shortest string that round-trips exactly
(like Python's repr), then keep the existing mantissa/exponent formatting.

Verified in a standalone compile of the exact function logic: the reporter's
values become 0.0174532925199433 and 1.E-05, 0.1 stays 0.1, and every tested
value (including a denormal) round-trips back to the identical double.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:14:24 +02:00
Petru Conduraru b1be7d92e6 ifcwrap: accept numpy scalars in aggregate type check #5873
check_aggregate_of_type used an exact type comparison (element->ob_type ==
type_obj), so a numpy array was rejected because its elements are numpy scalars
(numpy.float64) rather than direct float instances. For the numeric types,
accept subclasses: PyFloat_Check for double (numpy.float64 subclasses float) and
PyLong_Check (excluding bool) for int. The SPF REAL vs INTEGER distinction is
kept, so a float is not accepted where an int is expected and vice versa.

This replaces the earlier Python-side walk() approach, which the maintainer
preferred not to take since walk() is removed in v0.9. Verified with a runtime
red-green (built as a shared lib, called via ctypes): the old check rejects
np.array([3.0, 4.0]) and the new one accepts it, plain lists still work, an int
list is still rejected where a REAL is expected, and bool is rejected for INTEGER.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:12:41 +02:00
Petru Conduraru d2381ad6c6 IfcParse: don't strip delimiters from a single-character token #5683
asStringRef removes the first and last characters of a string, enumeration
or binary token to drop the delimiters, guarded only by !str.empty(). A
malformed single-character token (e.g. a bare '.' left when a fuzzer turns
'.PHYSICAL.' into '.)HYSICAL.') has length 1, so the first erase empties the
string and the second erase(str.begin()) runs on an empty string. That is
undefined behaviour: benign on a normal build, but it aborts (or throws
std::length_error from a later append) under a hardened libstdc++ with
_GLIBCXX_ASSERTIONS, which is why this file only segfaulted on the Fedora
build. Require at least two characters before stripping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:28:17 +02:00
Petru Conduraru 5e539890f1 buildinfo: report the release version instead of a hardcoded fallback #8164
When ADD_COMMIT_SHA is off (the default for release tarballs), buildinfo.cpp
fell back to a hardcoded "0.8.0", so a 0.8.5/0.8.6 build reported 0.8.0 from
IfcConvert --version and in written file headers. Pass CMake's RELEASE_VERSION
(read from the VERSION file) to IfcParse as IFCOPENSHELL_VERSION_STRING and use
it as the fallback, mirroring how the branch/commit defines are handled. The
commit-sha build and the last-resort literal are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:30:26 +02:00
falken10vdl 6b4c0194ff Merge pull request #7093 from falken10vdl/group_drawings_in_groups_and_dcos
Organize drawings under a parent called DRAWINGS in ifc groups and ifc documents
2026-07-06 08:40:49 +02:00
Bruno Postle 1614791775 Fix SHAPELY fill mode dropping surface fills for all but the last linked file
generate_linework() loops over the main file plus any linked models
(added in 5db955d4), reassigning drawing_elements each iteration. The
SHAPELY fill pass builds elements_with_faces/raycast_objs from
drawing_elements, but did so once *after* that loop finished, so it
only ever saw whichever file was processed last -- silently dropping
surface fills for every other file, including the main model whenever
any link was loaded.

Accumulate elements_with_faces/raycast_objs across every file inside
the loop instead of capturing drawing_elements once after it ends.

Generated with the assistance of an AI coding tool.
2026-07-05 23:45:43 +01:00
Bruno Postle b549e65ad9 bonsai: restore descriptive name for test_copy_with_new_geometry_copied_from_the_old
The underlying bug (has_material_styles bypassing the tool layer) was
already fixed by e76455913, which added the required mock expectation
here, but left the test under its quarantine placeholder name
test_AAAAAAAAAAAA. Restore the real name now that it genuinely passes.
2026-07-04 17:38:13 +01:00
Bruno Postle ee5d672493 tests: fix test_memusage_partial_open and add psutil to CI
test_memusage_partial_open was silently skipped in CI (psutil was
never installed there). Add psutil so it actually runs, and run the
RSS measurement in a subprocess so the fixture file isn't already in
the page cache from earlier tests, which was making both deltas read
as zero.

Generated with the assistance of an AI coding tool.
2026-07-04 17:32:44 +01:00
Bruno Postle 135f4cf023 tests: skip mathutils tests on Python < 3.13
mathutils only ships pre-built wheels for Python 3.13+ (verified
against PyPI's file list); on CI's Python 3.11, `pip install
mathutils` falls back to a slow/unreliable source build. Skip the
mathutils-dependent tests when the interpreter is too old instead.
2026-07-04 17:29:10 +01:00
Bruno Postle 608d9ead0e ci: add test coverage for ifc5d, ifcquery, ifcedit, ifcmcp
These packages already have their own pytest suites (ifc5d, ifcedit,
ifcmcp, ifcquery) but were never run in CI, so regressions in them
went unnoticed. Add path triggers and test steps for all four, plus
odfpy and xlsxwriter which ifc5d's spreadsheet export tests need and
mcp which ifcmcp's server tests need.

Generated with the assistance of an AI coding tool.
2026-07-04 17:22:42 +01:00
Bruno Postle a0ce930994 ifc5d: fix two csv2ifc bugs found by round-trip test
ItemIsASum and Quantities are exporter columns that were missing from
MAIN_CSV_HEADER_COLUMNS, causing them to be misidentified as numeric cost
value categories on re-import. Also initialise rate_cost_schedule to None
before the search loop to avoid UnboundLocalError when no match is found.

Generated with the assistance of an AI coding tool.
2026-07-04 16:08:43 +01:00
Bruno Postle eafa158ca0 Allow drawing generation in background mode
is_drawing_active() required an open VIEW_3D area purely as a poll()
gate for bim.create_drawing, even though SVG generation is
ifcopenshell.geom-based with no viewport dependency; skip that check
when bpy.app.background is true, since a viewport is neither
obtainable nor meaningful there. Interactive behaviour is unchanged.

Generated with the assistance of an AI coding tool.
2026-07-04 09:34:05 +01:00
falken10vdl 20b68ce0a9 Organize drawings under a parent called DRAWINGS in ifc groups and ifc documents 2026-07-03 14:52:29 +02:00
Thomas Krijnen b441fada90 Merge branch 'ifcviewer-wgpu' of https://github.com/IfcOpenShell/IfcOpenShell into ifcviewer-wgpu 2026-07-03 13:35:06 +02:00
Thomas Krijnen c9ee7695f3 include windows.h 2026-07-03 13:29:08 +02:00
Thomas Krijnen 1e032d188f boost math constants 2026-07-03 13:28:53 +02:00
Thomas Krijnen cdd1ecd15c Update parse examples 2026-07-03 13:28:02 +02:00
Thomas Krijnen 1c69f41e0a Examples now also depend on helpers 2026-07-03 13:27:26 +02:00
Thomas Krijnen 971cef0170 Schema dispatch in helpers 2026-07-03 13:26:30 +02:00
Bruno Postle 5db955d40c Apply link matrix when serialising linked drawings
Linked IFC files were included in SVG output but without their
world transform, causing geometry to appear at wrong coordinates.
Falls back to no transform if the link cache is unavailable.

Generated with the assistance of an AI coding tool.
2026-07-03 11:30:31 +01:00
Bruno Postle df27f86237 Fix linked drawings hidden on drawing activation
Link empty handles were missing from visible_objects, so linked
models were always hidden when activating a drawing.

Generated with the assistance of an AI coding tool.
2026-07-03 11:25:19 +01:00
Thomas Krijnen 55e97e5379 Style 2026-07-03 12:14:15 +02:00
Dion Moult a7f6aaa725 docs: rewrite viewport architecture page + add .ifcview format reference
Rewrite the BonsaiViewer viewport architecture page to match the current
renderer (updated type names, streaming/sidecar flow). Add a dedicated
.ifcview sidecar format reference page and link it from the ifcopenshell
formats toctree, and polish the Bonsai intro copy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:26:45 +10:00
Dion Moult 66d558ec2d ifcviewer: rename sidecar transfer/record types; drop unused element hierarchy (sidecar v17)
Rename the streamer/sidecar transfer and record types to describe what
they are rather than how they move:

  MeshChunk         -> StreamedMesh
  InstanceChunk     -> StreamedInstance
  InstanceCpu       -> InstanceInfo
  PackedElementInfo -> ElementTableRecord
  uploadMeshChunk   -> uploadStreamedMesh
  uploadInstanceChunk -> uploadStreamedInstance
  buildMeshChunk    -> buildStreamedMesh

and the two post-index sidecar metadata blocks:

  "critical" metadata -> "geometry" metadata  (meshes/instances/georef/TOC)
  "deferred" metadata -> "element"  metadata  (elements + string table)
  parseSidecarCritical -> parseSidecarGeometryMetadata
  parseSidecarDeferred -> parseSidecarElementMetadata

The one behavioural change: the element hierarchy (parent_id) was
carried through ElementInfo, ElementTableRecord, and the sidecar element
table but never consumed, so drop it and bump SIDECAR_VERSION 16 -> 17.
No back-compat: regenerate sidecars. sample.ifcview is regenerated at v17.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:26:31 +10:00
Dion Moult da5c0b7991 ifcviewer: section-plane cut tool on web — shared gizmo, true-face pick, drag/Del
Full section tool for the web viewport, with the gizmo + interaction shared with
desktop from one codebase.

- True-face surface pick. pickSurfaceAt had always ray-cast the instance AABB (to
  skip a depth readback), so cuts sat in front of the real surface. The pick
  fragment already computes the exact world_pos (it clips sections with it); now
  it OUTPUTS it to a 3rd pick MRT (RGBA32F) that every pick path renders, and
  pickSurfaceAt / pickSurfaceAtAsync read it back (decodeMappedPickPosition;
  ray-AABB kept only as a fallback). The web async pick chains id -> normal ->
  position spontaneous staging maps.
- Web tool: LMB drops a cut at the picked surface (LMB drag still orbits), K
  toggles, Shift+K clears; oriented to the real MRT surface normal. Exports + a
  Section / Clear cuts toolbar pair.
- Shared gizmo: lifted the section-gizmo renderer (SECTION_WGSL + thick-line AA +
  quad+arrow VBO + pack + screen-space hit-test) out of the Qt-coupled
  OverlayRenderer into a Qt-free SectionGizmoRenderer that ViewportCore::render
  draws for BOTH desktop and web (both already render via render()). One identical
  gizmo; OverlayRenderer's now-dead section code removed. Fixed 1 m size (matches
  the desktop constant).
- Interaction (shared): hitTestSectionGizmo (SectionGizmoRenderer::hitTest) +
  beginSectionDrag / updateSectionDrag / endSectionDrag live in ViewportCore.
  Drag a gizmo arrow to slide the plane along its normal; Del/Backspace removes
  the most recent cut. Desktop's ViewportWindow dropped its duplicate hit-test /
  drag math + state and delegates to the core; web wires the same calls.

Tests: sectionPlaneCount add/clear/cap (Catch2, 125); web smoke "click a surface
cuts geometry, clear restores" exercises the shared gizmo + 3-MRT pick (11/11).
Desktop object-pick / marquee unaffected; BonsaiViewer builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:20:50 +10:00
Dion Moult 9ad10c009b Add Bonsai Viewer about license
Replace the settings About placeholder with product, GPL, and third-party license information.

Generated with the assistance of an AI coding tool.
2026-07-03 19:20:50 +10:00
Dion Moult a14cecf68b Document viewer test commands
Add IfcOpenShell-Python and IfcViewer test-running documentation, including desktop CTest targets and web Playwright smoke tests. Move the web test README content into the Sphinx docs.\n\nGenerated with the assistance of an AI coding tool.
2026-07-03 19:20:50 +10:00
Dion Moult 791ff26697 Update Bonsai viewer licensing docs
Update Bonsai viewer headers to identify Bonsai and GPL licensing, add Bonsai Viewer documentation, and document debug output capture.

Generated with the assistance of an AI coding tool.
2026-07-03 19:20:50 +10:00
Dion Moult cd3d70172b ifcviewer: marquee box-select on web + suppress the canvas context menu
Bring rubber-band box-select to the web on the Web preset's select button (RMB).

- Core: factor the pick-pass encode + rect copy out of picksInRect into
  encodeBoxPickToStaging (mirroring how single-pick shares
  encodePickReadbackToStaging), shared by the sync picksInRect (desktop) and a
  new async picksInRectAsync (web) — the latter maps the staging buffer via a
  spontaneous callback because the sync spin-map hangs the JS loop. New
  applyMarqueeToSelection (plain replace / Shift add / Ctrl remove).
- Web main_web: a select-button drag past the click threshold draws a marquee
  rubber-band (a plain DOM <div> positioned in CSS px — no GPU overlay pass,
  which the web lib lacks) and on release box-picks the rect (device px) and
  applies it to the selection. A click (no drag) still single-picks.
- Web shell.html: the #marquee div + styling, and — the reported bug — a
  contextmenu preventDefault on the canvas so RMB (now the select button) doesn't
  pop the browser menu. (Firefox still forces its native menu on Shift+RightClick;
  that's a browser escape hatch pages can't override.)

Tests: applyMarqueeToSelection replace/add/remove + id-0 (Catch2, 124 total);
web smoke marquee drag → rubber-band shown → selection changes → hidden (10/10).
Desktop picksInRect unchanged in behaviour; BonsaiViewer builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:20:50 +10:00
Dion Moult d08c53d756 Remove stale WGPU mac workflow
Drop the obsolete diagnostic macOS WGPU workflow that referenced removed standalone viewer paths and targets.\n\nGenerated with the assistance of an AI coding tool.
2026-07-03 19:20:50 +10:00
Dion Moult cb1ab9cbfb Remove wgpu memory probe
Drop the standalone WGPU memory-allocation diagnostic and its conditional CMake target.\n\nGenerated with the assistance of an AI coding tool.
2026-07-03 19:20:50 +10:00
Dion Moult 8dfe00cdf8 Improve viewer variable names
Rename short local variables and parameters in the viewer loading, sidecar, and BonsaiViewer command paths to make their responsibilities clearer.\n\nGenerated with the assistance of an AI coding tool.
2026-07-03 19:20:50 +10:00
Dion Moult f1d97ac5aa ifcviewer: preset-driven nav mouse bindings + a "Web" preset (desktop + web)
Make orbit/pan/select mouse bindings pure data owned by ViewportCore so both
hosts and every preset share one source of truth, and add a "Web" preset. This
rounds out the matrix: the desktop gains a web-style scheme and the web inherits
all presets, with no per-platform hardcoding.

- Core: NavBindings { orbit, pan, select button + modifier } + setNavPreset
  ("blender" default | "rhino" | "revit" | "web") + navBindings(). Select is
  preset-driven too (was hardcoded LMB) so "web" moves it to RMB. web = orbit
  LMB, pan MMB, select RMB (LMB drag orbits with no click/drag ambiguity; RMB
  click-selects / drag-marquees). NavMod uses "Plain" not "None" (X11 #defines
  None to 0L).
- Desktop ViewportWindow: applyNavPreset sources the core table (mapped to Qt);
  marquee-arm / single-pick dispatch keys off select_button_. Default stays
  blender → no behaviour change.
- Desktop config: AppSettings::NavPreset gains Web + navPresetName(); the
  Settings dialog lists it. This also FIXES a pre-existing gap — the preset combo
  was persisted but never applied (only WGPU_NAV_PRESET env worked). MainWindow
  now applies the persisted preset at startup (env override still wins) and live
  on navPresetChanged, so all four presets actually work from the dialog.
- Web main_web: classifyPress routes the pressed button through navBindings()
  (orbit/pan/select), defaulting to the "web" preset; context menu already
  suppressed so RMB is free.

Tests: setNavPreset table (Catch2, 123 total); web smoke select tests use RMB.
BonsaiViewer builds; 9/9 web smoke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:20:50 +10:00
Thomas Krijnen a54a8b80d3 Rename to helpers 2026-07-03 11:18:57 +02:00
Thomas Krijnen 5873b05b84 Unify zstd lookup 2026-07-03 10:54:18 +02:00
Thomas Krijnen cf05bbd1bb Merge branch 'datamodel-v1.0' into ifcviewer-wgpu 2026-07-03 10:30:16 +02:00
falken10vdl 0aaafaedc9 Merge pull request #8014 from falken10vdl/surface-style-small-fixes
Surface styles fixes and message warnings
2026-07-03 09:27:12 +02:00
falken10vdl 7881f5992f Add warning when PHYSICAL/NOTDEFINED uses IfcColourRgb for Metallic, because this value is IFC-only and does not affect Blender appearance. 2026-07-03 09:02:57 +02:00
falken10vdl 679fe4dcae Add warnings for emissive and specular ratios in FLAT reflectance method (IFC only no Blender appearance) 2026-07-03 09:02:57 +02:00
falken10vdl a89621b179 Fix Lighting/refraction UI drawing crashes and add warning message that they are only IFC data not used by Blender for surface appearance 2026-07-03 09:02:57 +02:00
falken10vdl 110e4050c8 Add warning messaging for unsupported reflectance methods and texture modes 2026-07-03 09:02:56 +02:00
falken10vdl 6dafb7a5c2 Avoid duplicate image datablocks when loading textures 2026-07-03 09:02:56 +02:00
falken10vdl 4cedeec813 Fix FLAT+EMISSIVE texture loading crash 2026-07-03 09:02:56 +02:00
falken10vdl 6314d9c818 avoid full shader rebuilds in intermediate property write 2026-07-03 09:02:56 +02:00
falken10vdl 9bbd2b1854 Allow UV mode selection in Loader and add UI warning for SOLID Mode (no Generated or Camera UV) 2026-07-03 09:02:56 +02:00
falken10vdl b5d36aacf6 Load styles after removing surface style in RemoveSurfaceStyle operator so UI List is updated 2026-07-03 09:02:56 +02:00
sboddy 6d3bed1f7d Merge pull request #8238 from sboddy/copilot/featurecamera-shift-xy-drawings
Implement #5628 - Camera X/Y shift for perspective drawings
2026-07-02 21:14:55 +01:00
Ryan Schultz 0096c0f6a2 Bonsai: don't wipe link query when reload_link is called without one
bpy.ops.bim.reload_link(link_index=...) from a script skips invoke(),
so self.query stayed at its empty default and execute() overwrote the
link's stored query, reloading everything. Only update link.query when
the property was explicitly set (dialog or script argument), and reload
using the stored query.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:50:21 -05:00
Stephen Boddy 661be0d66d Tweak the Copilot generated UI code so it looks better 2026-07-02 19:59:52 +01:00
copilot-swe-agent[bot] 62ed650b75 Polish camera shift support 2026-07-02 17:47:30 +00:00
copilot-swe-agent[bot] da4b2f6eee Add camera shift sync 2026-07-02 17:46:11 +00:00
Gorgious56 00ec587296 Bonsai: persist link IFC query across reload
Store the selector query used at Link IFC time on the Link
PropertyGroup, restore it from the sidecar cache JSON on host
IFC reopen, and forward it through LoadLink and ReloadLink so
subsequent reloads replay the original filter instead of loading
every element. ReloadLink now opens a small dialog pre-populated
with the current query, allowing the user to edit it in place
without unlink-and-relink.

Also swap TestCalculateLinkMatrix off NamedTemporaryFile(delete=True)
which held an exclusive Windows handle and blocked the
code-under-test from reopening the sidecar path.

Closes #8219

Generated with the assistance of an AI coding tool.
2026-07-02 14:04:29 +02:00
Eivind Pagander Tysnes 2a05528b6d Documentation: Introduction to Ifc changed
After feedback on PR changed tip to be a single tip for easier and
more correct reading.
Removed 3 trailing backspaces
2026-07-02 21:39:22 +10:00
Eivind Pagander Tysnes a5f7f0cd93 Updated documentation
Updated the introduction to IFC to have IFC 4x3 be a published
version from 2024 and updated tips to recommend using IFC 4x3
for infractructure
2026-07-02 21:39:22 +10:00
Gorgious56 1fd7329122 Merge pull request #8234 from Gorgious56/fix-apply-opening-crash-on-non-fillings
Bonsai: fix Apply Opening crash on non-fillings
2026-07-02 12:11:06 +02:00
Thomas Krijnen f3e047d78e Schema compatibility #8230 2026-07-02 12:01:57 +02:00
Gorgious56 041306c5f0 Bonsai: extract is_filling_supported + guard aggregate hosts
Fold two related cleanups from post-PR review into one commit:

Shared filling predicate — the gizmo poll and AddOpening._add_openings
both need to decide whether an IFC entity is a Bonsai-supported filling
(IfcDoor / IfcWindow, the classes the opening generator can derive
geometry from). Centralise the check in bim.module.model.opening as
is_filling_supported so a schema-broadening tomorrow only edits one
predicate. The gizmo's own predicate is renamed
is_supported_filling_or_opening to reflect its wider domain (also
accepts None for raw meshes and IfcOpeningElement for reassignment).

Aggregate-host guard — regenerate_filling_opening_body returns the
voided host Blender object so callers can recut it. Aggregates have
no mesh data; returning them made callers hit switch_representation
against a None data-block. Guard on voided_obj.data is None and
return None so callers can skip cleanly.

Adds a direct position_gizmos test asserting host-at-index-1 (filling
active) still anchors on the slab — pins the class-based dispatch's
selection-order independence.

Generated with the assistance of an AI coding tool.
2026-07-02 11:44:33 +02:00
Gorgious56 5fba0026dd Bonsai: fix ruff import-sort drift in geometry+model ui
Both files interleaved bpy.types imports with ifcopenshell.util
imports, which ruff's I001 rejects for standard-library / third-party
ordering. Running ruff check --fix on the two files reorders them into
the isort-canonical shape with no behaviour change.

Generated with the assistance of an AI coding tool.
2026-07-02 09:19:50 +02:00
Gorgious56 4d92a64206 Bonsai: skip sibling refresh on show/hide toggle
EditOpenings.edit_openings unconditionally walked sibling wall sets
twice on every processed opening — once by mapped source id via
get_similar_openings_building_objs, once by filling type via
get_all_building_objects_of_similar_openings — and unioned both into
the building_objs recut set. reload_body_representation then hit every
one of those walls with a switch_representation call, even for the
show/hide toggle path where nothing about the opening changed.

Move both sibling-wall unions inside the is_edited / is_moved branch.
Pure show/hide (no shape edit, no move) now touches only the wall(s)
directly hosting the toggled openings. The edit and move paths still
refresh siblings the same as before, since a mapped-source rewrite
propagates the new shape to every sharing wall and each one needs a
recut.

Generated with the assistance of an AI coding tool.
2026-07-02 09:18:13 +02:00
Gorgious56 9d2de117a9 Bonsai: sync filling placements on wall regen
recalculate_walls commits the wall's own placement to IFC before
recreating its geometry but did not touch its fillings. A door moved
along the wall's reference line therefore stayed cut at its old
position when the user pressed SHIFT+G on the wall, because the wall
recut ran against the still-stale opening placement in IFC.

Walk each wall's HasOpenings and, for every filling whose Blender
matrix_world differs from its committed IFC placement (tool.Ifc.is_moved),
commit the filling's placement and propagate the new matrix to the
enclosing opening via ifcopenshell.api.geometry.edit_object_placement.
The subsequent recreate_wall pass then sees the fresh opening positions
and cuts at the right spot.

Generated with the assistance of an AI coding tool.
2026-07-02 09:13:04 +02:00
Gorgious56 6ee3c7a15f Bonsai: restore opening regen on recalculate_fill
Commit 82dd1d94d switched RecalculateFill from
bonsai.core.geometry.switch_representation to the surgical
tool.Geometry.recut_host to speed up batched host recuts. The trade-off
was intentional for that scope but dropped the implicit opening body
refresh that switch_representation used to provide: SHIFT+G on a door
whose parametric dimensions had drifted from its opening no longer
resized the opening, so the wall recut still hit a stale mapped source.

Extract a targeted single-source helper on tool.Model
(regenerate_filling_opening_body) that regenerates one filling's
mapped opening body via the existing FilledOpeningGenerator and
inverse-substitutes the new representation across every filling that
shares the mapped source. Refactor the family-wide caller
(update_simple_openings, used by the parametric-edit finish path) to
delegate to the same helper, deduped by source id so fragmented type
families where multiple mapped sources coexist all get refreshed.

Call the targeted helper at the top of RecalculateFill._recalculate_fills
for each distinct source among the selected fillings. All body-
representation lookups go through tool.Geometry.get_body_representation
rather than inlining the ("Model", "Body", "MODEL_VIEW") triple. An AST
forward-compat guard pins the call site.

Generated with the assistance of an AI coding tool.
2026-07-02 09:08:09 +02:00
Gorgious56 fdf9970685 Bonsai: fix Apply Opening crash on non-fillings
The + gizmo previously appeared whenever a fillable host and any
non-host object were selected, so clicking it against an IfcCovering
crashed the geometry kernel when the opening generator tried to derive
a shape it couldn't build (AttributeError on 'NoneType.wrapped_data').

Tighten the gizmo poll to require the secondary selection to be a
class the operator can dispatch on: IfcDoor, IfcWindow,
IfcOpeningElement, or a non-IFC mesh. Make the poll selection-order-
independent so either click order activates it. Validate the same
class set at the operator boundary so keymap or scripted invocations
report a clear warning instead of crashing.

The narrower Door/Window support in the opening generator is a Bonsai
implementation limit, not an IFC schema restriction —
IfcRelFillsElement.RelatedBuildingElement is typed as IfcElement and
the schema permits any subtype. The tooltip and inline comment on the
validation branch note this so a future reader knows the gate is
future-work, not schema-mandated.

Rewrite the operator's bl_description to end-user-friendly wording that
drops the internal terms matrix_world and rl1/rl2.

Fixes #8215.

Generated with the assistance of an AI coding tool.
2026-07-02 09:02:02 +02:00
Massimo Fabbro de65e50fb5 See #6570. Formula column other improvements 2026-07-02 08:40:58 +02:00
Massimo Fabbro 714105b9fd See #6570. Tests for import cost schedule from csv and minor fix 2026-07-02 08:40:58 +02:00
Massimo Fabbro f0b5ab860f See #6570. Formula column minor improvements and documentation 2026-07-02 08:40:58 +02:00
Massimo Fabbro 528964ca56 See #6570. Formula column for ifc5d import from csv
Now it's possible to specify the Formula column in the csv in order to calculate cost item quantities
2026-07-02 08:40:58 +02:00
Massimo Fabbro 2c2d0f2434 See #6570. Now it's possible to specify the formula in cost item quantity assignment 2026-07-02 08:40:58 +02:00
Dion Moult 4faae025b4 ifcviewer: tests for the new viewing features (fly, x-ray, visibility)
Coverage had lagged the recent feature work. Add both layers:

- test_viewport_camera (Catch2, headless): constructs ViewportCore with a mock
  ViewportHost — construction/teardown touch no GPU (the wgpu teardown lives in
  releaseWgpuModelGpuData, only reached with models loaded), and the camera ops
  are pure — so it unit-tests the SHARED fly math fast and deterministically:
  flyMove (forward step, 5x boost, opposing-key cancel, dt clamp, QE along +Z,
  degenerate-pitch stays finite), flyLook (turn-in-place pins the eye, pitch
  clamp), flyAdjustSpeed (x1.25/notch, [0.05,1000] clamp), toggleXray, and
  hideSelected/showAll. Links the built IfcViewerCore (ViewportCore.cpp is the
  monster TU that can't compile standalone). +9 cases → 122 desktop.

- smoke.spec.mjs (Playwright): fly (enter → W moves the camera → Esc exits),
  x-ray (toggle translucency on/off), and hide-after-pick, driving the exported
  C hooks end-to-end. +3 cases → 9 web.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:23:30 +10:00
Dion Moult 68d6280c93 ifcviewer: visibility (hide/isolate/show-all) + X-ray — desktop parity on web
Lift the visibility + X-ray ops into ViewportCore so desktop and web share them.
The cull already reads visibility_ (hidden objects skipped) and the frame uniform
reads xray_alpha_cap_, both per frame, so each op just mutates state + schedules
a frame — no GPU buffers to rebuild, no rendering work:
- hideSelected (hide the selection, then deselect), isolateSelected (hide every
  non-selected object in a visible model), showAll (clear the hidden set),
  toggleXray (flip the alpha cap 1.0<->0.3; cull routes all to the transparent
  pass), xrayActive().
- Desktop ViewportWindow: the H / Shift+H / Alt+H / Alt+X keys and the menu-action
  wrappers now call the core; the four inline/duplicated implementations are gone.
- Web main_web: same keys (H hide · Shift+H isolate · Alt+H show all · Alt+X
  x-ray) + toolbar buttons (Hide / Isolate / Show all / X-ray, the last reflecting
  active state).

Verified on web: x-ray toggles and visibly translucent-izes the scene, hide after
a pick removes geometry, 0 GPU errors. 113/113 desktop + 6/6 web smoke; desktop
app builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:47:03 +10:00
Dion Moult bdfa70468e ifcviewer-web: frame the first model only on load, matching desktop
applyCachedModel already does a one-time viewAll gated by initial_view_applied_
(desktop + web) — it frames the FIRST model loaded and never re-frames as more
arrive. Two web-only divergences from the desktop viewer, fixed here:

- loadSidecarMetadataWeb called viewAll() AGAIN, unconditionally, per model, so
  every federated model that streamed in yanked the camera back to fit the whole
  scene. Drop the redundant call; the shared gate handles first-model framing.

- Nothing ever cleared initial_view_applied_. On web the embedded sample sets it
  at startup, so after clear_scene_c (a ?model= federation load) the gate was
  already tripped and the loaded models never got framed — the camera stayed on
  the prior view. (The redundant per-model viewAll above masked this until it was
  removed.) Clear the flag in resetScene: a fresh scene should auto-frame its
  first model — correct for desktop fresh-opens too.

113/113 desktop + 6/6 web smoke pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:58:51 +10:00
Dion Moult f9acf8be3a ifcviewer: first-person / fly view — share the desktop fly camera with web
DRY the fly camera into ViewportCore so desktop and web share identical math:
- flyMove(fwd,back,right,left,up,down,boost,dt): WASD in the view plane, QE
  along world +Z, forward = eye→target (no snap after orbiting), Shift = 5x,
  dt clamped to 0.1s. flyLook(dx,dy): turn in place (yaw/pitch, eye pinned,
  0.2 deg/px, pitch ±89.9). flyAdjustSpeed(notches): wheel scales speed x1.25.
  fly_move_speed_ + orbitEye now live in the core (orbitEye's duplicate removed
  from ViewportWindow).
- Desktop ViewportWindow: fpsIntegrate / mouse-look / wheel-speed call the core
  methods; behaviour unchanged, BonsaiViewer builds clean.
- Web main_web: Shift+F (or the Fly toolbar button) enters and pointer-locks the
  canvas; W/A/S/D/Q/E + Shift are held-tracked (keydown/keyup) and integrated
  each RAF frame with wall-clock dt; pointer-lock mouse deltas drive flyLook;
  wheel tunes speed. Exit on Esc OR a canvas click (matches desktop) — a
  pointerlockchange handler catches the browser eating the first Esc to release
  the lock (so a single Esc exits), guarded by fly_locked so a denied lock on
  entry doesn't insta-exit. Fly button reflects/ syncs active state.

Verified: web enter→WASD moves→click/Esc exits, 0 GPU errors; 113/113 desktop
core + 6/6 web smoke; desktop app builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:04:35 +10:00
Dion Moult 0b8c787ac0 ifcviewer: v16 zstd-compressed sidecars (~10x smaller over the wire)
The .ifcview data is hugely redundant (repeated double instance matrices,
patterned indices) — measured 12x zstd whole-file. Server Content-Encoding
can't be used (it breaks HTTP Range), so compress PER-CHUNK into the format.

Format (v16): geometry becomes per-chunk zstd(vertices)+zstd(indices) frames —
each independently Range-fetchable, so streaming is intact — and the critical +
deferred metadata blocks are single zstd frames. SidecarChunk carries the
compressed blob offsets/sizes; applyStreamedChunk (render/upload) is UNCHANGED —
decompression slots into the fetch. Full readSidecar (test/tooling) reconstructs
by decompress+scatter. zstd: desktop links libzstd (also compresses at bake);
the web build (Emscripten has no zstd port) FetchContent's the pinned zstd
source and compiles its decompress-only subset for wasm — no vendored blob,
same version as desktop. New SidecarCompress wraps it (compress guarded off
under Emscripten). Both stream paths — desktop StreamingThread worker + sync
fallback (readChunkGeometryCompressed) and web beginWebChunkLoad — decompress;
readSidecarMetadataOnly / the web bootstrap / loadDeferredMetadataWeb decompress
the metadata blocks. streamingByteProgress reports COMPRESSED bytes. MEASURED: a
752 MB v15 federation → 75 MB v16 (10x; per-file 6.7-15.3x); PP-PLP 118→15 MB,
loads 13/13 chunks on web, 0 errors.

Three fixes found while testing big federations on a real server:
- Web-streamed race: streaming_from_web was set in the deferred-header callback
  (a round-trip after the model+chunks exist), so driveStreamingLoads could take
  the sync fopen path meanwhile → "failed to read/decompress chunk 0". Now set
  immediately after applyCachedModel.
- OOM abort on 18 models: the pool grew unbounded until an alloc failed, but on
  web that's an uncatchable bad_alloc abort. Cap total pool capacity
  (setMaxTotalCapacity, 3 GB) so it stops before the heap ceiling, and raise
  MAXIMUM_MEMORY 2→4 GB (wasm32 max) for headroom.
- Web never evicted (grow-or-block only). At the hard budget, fall through to the
  LRU/priority evictor so a big federation stays navigable (highest-contribution
  chunks win) instead of freezing with holes.

113/113 desktop + 6/6 web smoke pass. No back-compat: regenerate sidecars
(desktop bakes v16; scratch conv tool migrates v15→v16).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:03:19 +10:00
Ryan Schultz 340d4fb82a Honor ApplicableOccurrence in is_relating_type_compatible
Companion to the assign_type.py fix. The same class-pairing validation
added in 10ee5aef4f also gates the Bonsai-side type assignment UI via
tool.Type.is_relating_type_compatible, which the AssignType operator
uses to filter selectable objects. For annotation types (abstract
IfcTypeProduct), get_applicable_types(IfcAnnotation) is empty, so every
annotation was skipped with "No selected object can be typed by
IfcTypeProduct."

Honor the type's ApplicableOccurrence attribute as a fallback, matching
the core API fix. occurrence.is_a() handles subtypes and returns False
for unknown tokens, so free-form text is not trusted blindly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 11:03:35 +02:00
Ryan Schultz 65cd5701c3 Honor ApplicableOccurrence in assign_type class validation
The class-pairing validation added in 10ee5aef4f rejected every typed
annotation with "IfcTypeProduct cannot type IfcAnnotation ... (allowed
occurrence classes: <none>)".

The check derived allowed occurrence classes solely from the
buildingSMART implementer-agreement map, which has no entry for the
abstract IfcTypeProduct that Bonsai uses for annotation types (IFC4 has
no IfcAnnotationType). The intended occurrence class is declared in the
type's ApplicableOccurrence attribute (e.g. "IfcAnnotation/TEXT"), the
schema-defined mechanism for exactly this purpose.

Augment the allow-list with the ApplicableOccurrence class, but only
when its leading token resolves to a real entity in the schema so
free-form text is not trusted blindly. Genuine mismatches (e.g.
IfcWallType -> IfcWindow) are still rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 11:03:35 +02:00
Dion Moult 5299f6c13c ifcviewer-web: contribution-cull streaming + combined loaded/needed/total bar
Streaming picked candidates on raw frustum visibility, so viewAll over a big
federation (all models in-frustum) fetched every chunk — even fine chunks that
project to sub-pixel and the renderer never draws. Gate streaming on the SAME
contribution decision the render path already makes.

- New per-chunk contribution_visible_count: instances that passed frustum AND
  the contribution cull (projected radius >= min_radius_px), counted BEFORE HiZ
  — so it's stable while the camera is still (unlike the HiZ-post counters,
  which flip frame-to-frame and would thrash the loader) and shifts only on
  navigation, when the working set should. The candidate gate skips chunks with
  count 0: the network pulls only what's resolvable now; the rest stream in as
  you approach. Verified: fit view needs the geometry, zoomed-out needs 0.

- Loading UI reworked from per-model segments to a combined bar over the whole
  federation: dark track = not needed for this view, dim = needed-but-unloaded,
  bright = loaded. "loaded / needed" = how done THIS view is; "needed / total" =
  how much of the model the view requires — "Loading 45 / 90 MB for this view ·
  12% of 718 MB total". Driven by ViewportCore::streamingByteProgress + ifcv_bytes_*.
  Two fixes from testing: (1) show a distinct overhead phase while total==0
  ("Loading model data — X MB · Y/N models ready") so the metadata download
  isn't a dead-looking bar; (2) re-assert display:block every active frame so the
  bar REAPPEARS when navigation reveals new chunks (was only set in
  beginLoadProgress → stayed hidden after the first catch-up). Per-model exports
  kept for a future detailed view.

111/111 unit + 6/6 web smoke pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:28:44 +10:00
Dion Moult 001476c5a9 ifcviewer-web: navigation parity — view all, XYZ views, ortho, zoom-to-selected
The camera math already lived in the shared ViewportCore; the desktop just
bound keys to it. The web build had no keyboard handler and no camera UI, so
none of it was reachable. Wire it up (Tier 1 + 2 of nav parity; fly mode is a
separate follow-up).

Shared core:
- setStandardView(StandardView) — named Front/Back/Left/Right/Top/Bottom wrapper
  over setStandardView(yaw,pitch), so the axis→angle mapping lives in one place.
- frameSelection() — lifts the desktop's "union selected AABBs → frameAabb(1.30)"
  focus logic out of ViewportWindow into the core. Desktop's focusOnSelectedObject
  and the X/Y/Z hotkeys now call these (DRY, behaviour unchanged).

Web:
- main_web gains a keydown handler matching the desktop bindings — Home=view all,
  F=zoom to selected, P=ortho toggle, X/Y/Z (+Shift=negative)=standard views —
  plus exported entry points (view_all_c / frame_selection_c / toggle_projection_c
  / projection_is_ortho_c / standard_view_c) for the toolbar.
- shell.html adds a bottom nav toolbar (Fit / Focus / Persp-Ortho / the six views)
  with the hotkeys in tooltips; the ortho button reflects state.

Verified: Z key and Front button both move the camera, ortho toggles render +
label, zero GPU errors; 111/111 unit + 6/6 web smoke pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:42:45 +10:00
Dion Moult 10ab982032 ifcviewer-web: load many models from the URL + per-model loading panel
Query string: auto-load a whole federation, not one model. Accepts repeated
params (?model=a&model=b&…) and/or a comma list (?models=a,b,c); each becomes
its own streamed byte-source, clearing the embedded sample once then streaming
concurrently into one scene. A failed URL is reported without aborting the rest.

Loading UI: the bare aggregate chunk count didn't reveal the federation state,
so surface per-model progress. ViewportCore::streamingModelCount() +
streamingModelProgress(idx,…) (ordered by model_id = load order) feed
main_web's ifcv_model_count_c / ifcv_model_{resident,total}_c. shell.html draws
a panel: "Loading N models — X done · Y streaming · Z waiting · N MB" plus one
segment per model (blue fill while streaming, green when resident, grey while
its metadata is still pending) — so parallel loading and how many remain are
visible at a glance.

Verified: 10 models from a 10x ?model= query stream in parallel; the panel
steps 10 waiting → streaming → all done. 111/111 unit + 6/6 web smoke pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:11:02 +10:00
Dion Moult 4c38e52741 ifcviewer-web: multi-file loading (federation) via a per-model byte-source
The scene core is already multi-model — models_gpu_ is a map, applyCachedModel
APPENDS, and per-model model_id / object_id rebasing / georef+transformation
are how the desktop federates today. The only web-specific gap was the byte
source: web had ONE global source (__ifcvFile/__ifcvUrl) and reset the scene on
every load, so it could show one file at a time. Desktop meanwhile carries a
per-model source (streaming_file_path).

Mirror that on web: give each model its own web_source_id into a JS source
registry (Module.__ifcvSources[id] = a picked File or a sized remote URL).
beginWebChunkLoad, the metadata bootstrap, and the on-demand deferred fetch all
read from the owning model's source, so several files stream concurrently into
one federated scene — reusing all the shared machinery (viewAll, picking, the
GUID fetch) untouched.

- webReadRangesAsync / ifcvReadRangeInto / ifcvSourceSize take a source id.
- loadSidecarMetadataWeb(source_id, …) appends (no resetScene); main_web
  exposes load_sidecar_from_source_c(id) + clear_scene_c().
- URL size resolution moved to JS (shell.html registers + sizes sources via
  HEAD/Range), retiring the C-side ifcvBeginUrlSource / ifcv_source_ready dance.
- shell.html: source registry + "Open" (replace) / "Add" (append) buttons,
  multi-file selection; ?model= registers a URL source then loads.

Verified: two sidecars from two sources stream into one scene, both fully
resident, zero GPU errors. 111/111 unit + 6/6 web smoke pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:03:24 +10:00
Dion Moult 681de6f817 ifcviewer-web: pick logs the object's IFC GUID via the on-demand deferred fetch
First real consumer of the v15 deferred property block, and an end-to-end
demonstration that on-demand property loading works. On a left-click pick,
logSelectedObjectGuidWeb ensures the owning model's deferred block is loaded
(loadDeferredMetadataWeb — a network fetch the FIRST time, cached after) and
logs the picked object's GUID to the console.

Fix uncovered while wiring it: applyCachedModel rebases instance object_ids to
a per-model global base (object_id_base) to keep them unique across models,
but the deferred elements carry the sidecar's original local ids — so a lookup
by the picked (global) id missed. Store object_id_base on the model and rebase
the elements by it when the deferred block loads.

Verified: with a streamed model, the deferred block is fetched ONLY after the
first pick (not at load), and the pick logs a valid 22-char IFC GUID. 111/111
unit + 6/6 web smoke pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:21:33 +10:00
Dion Moult 13ec2be807 ifcviewer-web: streaming loading bar (metadata → geometry progress)
Users had no feedback during the seconds before/while a model streams. Add a
top progress strip + caption driven by the streaming state:
- "Loading model… N MB" while the critical metadata downloads (no chunks yet),
- "Loading geometry — R / T chunks · N MB" as chunks go resident,
- "Loaded — T chunks · N MB", then it hides.

ViewportCore::streamingProgress(resident, total) sums chunk residency across
models; main_web exports ifcv_chunks_resident_c / ifcv_chunks_total_c for
shell.html to poll each RAF. The EM_JS range reader accumulates
Module.__ifcvBytesLoaded so the caption can show MB downloaded. Shown only for
streamed loads (?model= URL / picked file), not the embedded sample.

6/6 web smoke pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 08:54:42 +10:00
Dion Moult 41a85a70ba ifcviewer: v15 — defer property metadata off the first-paint path
First-paint over a network is metadata-bound: the whole post-index metadata
(~10 MB on a 118 MB model) had to download before any geometry. But ~25% of
it — elements + string_table, the IFC element tree (names/GUIDs/hierarchy) —
is used only for UI/picking, never for rendering (ViewportCore never touches
it).

v15 splits the post-index metadata into a render-CRITICAL block (meshes,
instances, georef, chunk TOC) preceded by its byte length, then a DEFERRED
block (elements + string_table). The web loader reads only the critical block
before painting; the deferred block sits at a known, self-describing offset
([critical end, EOF)) and is fetched on demand. Desktop reads both (local).

Web on-demand path is wired and complete (not yet called — no UI consumer):
loadDeferredMetadataWeb(model_id) range-fetches + parses the deferred block
into ModelGpuData.elements/string_table, at most once; the first consumer
will be "show the selected object's name" on pick. No background prefetch —
view-only sessions never download the property data (saves 2.64 MB on this
model).

parseSidecarTail split into parseSidecarCritical + parseSidecarDeferred (pure,
unit-tested); StreamingSidecar gains the critical-block locator. Measured
(118 MB model): critical metadata 10.35 -> 7.71 MB, deferred 2.64 MB off the
path; first paint 10.5 -> 9.6 s @ 24 Mbps. (Instances still dominate the
critical block — the next metadata lever.) Format -> v15, no back-compat;
regenerate sidecars. 111/111 unit + 6/6 web smoke pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 08:01:15 +10:00
Ryan Schultz febde1bbbb Closes #8226: Add bulk-load of selected drawings' annotations
SHIFT+CTRL+CLICK on Activate Drawing now imports the
annotations of all selected drawings without switching
the active view or camera. The drawing camera is imported
when missing so annotations are collected into the correct
drawing collection. Loading is idempotent.

Generated with the assistance of an AI coding tool.
2026-06-30 15:23:23 -05:00
Ryan Schultz 315835063c Fix #8225: Respect camera boundary for Include filter
The drawing Include filter replaced the camera-view element set
entirely, so elements outside the camera boundary were drawn.
Intersect the filter results with the camera-view set instead.

Generated with the assistance of an AI coding tool.
2026-06-30 13:10:12 -05:00
Gorgious56 1b0a2fd6de Merge pull request #8222 from Gorgious56/viewport-decorator-lifecycle-sweep
Bonsai: migrate viewport decorators onto canonical lifecycle helper
2026-06-30 15:45:08 +02:00
Gorgious56 c95c1905ed Bonsai: draw georef gizmo above 3D model geometry
The georef orientation gizmo (crosshair, project-north arrow,
grid-north arrow, true-north arrow, WCS leader) is a coordinate-
system overlay: its purpose is to communicate orientation regardless
of what the model contains, so it must remain visible regardless of
whether 3D geometry occupies the gizmo's z=0 footprint.

Wrap GeoreferenceDecorator.draw_geometry's draw cycle in a
gpu.state.depth_test_set("ALWAYS") / restore pair so the overlay
draws on top of any 3D geometry between the camera and the gizmo.
Matches the precedent set by the dashed-line overlay in
bim/module/model/opening.py.

Generated with the assistance of an AI coding tool.
2026-06-30 15:36:03 +02:00
Gorgious56 301dae103c Bonsai: guard decorator draws against None and empty lists
Three crashes that surfaced when viewport decorators ran against
selected non-IFC blender objects or top-level objects with no
aggregate parent:

- WallAxisDecorator.draw_wall_axis: tool.Ifc.get_entity(obj) returns
  None for a non-IFC selection (default cube, lamp, camera). The
  subsequent element.is_a("IfcWall") raised AttributeError on every
  redraw. Guard with `element and element.is_a(...)`.
- _ConnectedNetworkPathDecorator flow-segment loop: same shape;
  iterates entries that may be None, calls .is_a("IfcFlowSegment")
  unconditionally. Same guard.
- AggregateDecorator.draw_aggregate: indexes aggregates_list[-1]
  unconditionally in the else branch; raises IndexError when the
  selected element has no aggregate parent. Also leaves `aggregate`
  unbound across loop iterations in the `in_aggregate_mode` branch
  when `index <= 0`. Define `aggregate = None` per loop iteration
  and guard the [-1] indexing with `elif aggregates_list:`.

Generated with the assistance of an AI coding tool.
2026-06-30 14:06:53 +02:00
Gorgious56 85cd1c1923 Bonsai: migrate viewport decorators onto canonical base
Migrate 17 legacy viewport decorators (ClashDecorator, SolarDecorator,
MeasureDecorator, ItemDecorator, GeoreferenceDecorator, NestDecorator,
NestModeDecorator, GridDecorator, LoadsDecorator, AggregateDecorator,
AggregateModeDecorator, PolylineDecorator, ProductDecorator,
WallAxisDecorator, SlabDirectionDecorator, FaceAreaDecorator,
BoundingBoxDecorator) from hand-rolled install/uninstall lifecycles
onto the canonical tool.Blender.ViewportDecorator base. The legacy
uninstall removed each handler from Blender but never cleared
cls.handlers, growing a stale-reference list across enable/disable
cycles. The base's uninstall clears the list correctly.

State-derived install methods (ItemDecorator, ProductDecorator,
LoadsDecorator, PolylineDecorator) keep an install override per the
base's documented contract.

Drop the now-redundant per-class draw_batch copies and the module-
or method-scope transparent_color defs in favour of the base helpers
introduced in the preceding commit. system/decorator.py and
boundary/decorator.py keep their installed-flag lifecycle (different
pattern, no leak) but consume tool.Blender.transparent_color.

Add an AST forward-compat guard pinning the contract structurally:
any class declaring handlers = [] (Assign or AnnAssign) must subclass
tool.Blender.ViewportDecorator. Add a runtime regression on
ClashDecorator's install/uninstall cycle.

Generated with the assistance of an AI coding tool.
2026-06-30 14:03:17 +02:00
Gorgious56 091fc9e7e5 Bonsai: add viewport decorator base helpers
Add two helpers to tool.Blender that 17+ existing viewport decorators
re-implement byte-identically:

- ViewportDecorator.draw_batch(shader_type, content_pos, color, indices=None)
  collapses the validate + batch_for_shader + uniform_float + draw cycle
  every shader-driven decorator needs.
- Blender.transparent_color(color, alpha=0.1) is the RGBA-alpha-override
  helper duplicated across nest, project, aggregate, model, system module
  scopes plus six nested-def copies inside draw methods.

Pure additions on the tool/ layer with direct unit tests covering the
default-alpha, explicit-alpha, non-mutation, new-list-instance, and
validation-guard branches.

Generated with the assistance of an AI coding tool.
2026-06-30 13:59:15 +02:00
Dion Moult e1be2f208c ifcviewer: v14 chunk-contiguous sidecar + progressive network streaming
Makes large-model streaming over a network actually good — fixing read
amplification, then first-paint latency — building on the byte-range work.

v14 layout + TOC (SidecarLayout, pure + unit-tested)
  The loader chunks meshes by spatial Morton order, but the sidecar stored
  geometry in mesh-id order, so a chunk's meshes were scattered through the
  file: streaming one chunk meant either hundreds of tiny range requests or
  reading (and discarding) everything between them — a 113 MB model fetched
  ~340 MB, a 531 MB model 2.25 GB (4.2x). Fix: at bake, reorder meshes into
  the loader's chunk order and rebuild vertex/index(LOD0+LOD1)/instance
  sections so each chunk is one CONTIGUOUS byte range, and bake a chunk TOC
  ({first_mesh, mesh_count}). The loader builds chunks straight from the TOC
  rather than re-deriving the plan — the float Morton quantisation isn't
  bit-identical across toolchains (x86 baker vs wasm loader), so a re-derived
  plan scatters the chunks. Format bumped to v14 (regenerate sidecars). The
  reorder buckets instances by per-instance mesh_id (the baker never sets
  MeshInfo.first_instance — trusting it scrambled every transform → geometry
  at the origin). Multiset-verified on a 28,900-instance model: every
  instance's placement + geometry preserved. Result: 531 MB fetches 531 MB
  (1.0x) in 72 requests (was 2036).

Progressive streaming (concurrency cap + small chunks)
  Even at 1x, geometry appeared only after ~the whole model arrived: the
  browser multiplexes every in-flight Range request over one HTTP/2 conn, so
  unbounded concurrency (9 in flight) split the bandwidth and nothing finished
  until the end (measured: first paint after 113 of 118 MB / 35 s @ 24 Mbps).
  Cap concurrent chunk loads (kMaxWebInflightChunks=2): the priority-sorted
  top chunks finish and paint first, then the next → first paint 9 s. Chunk
  size dropped 16->4 MB (cheap now that each chunk is one read; matches Cesium
  3D Tiles / xeokit / SVF2) for smoother progression. First-paint is now
  metadata-bound (~10 MB tail) — the next lever.

111/111 unit (new test_sidecar_layout: geometry preserved, contiguous layout,
Morton-identity) + 6/6 web smoke pass; desktop bake (SceneLoader) reorders
before writeSidecar; embedded web sample regenerated to v14.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 21:22:47 +10:00
Gorgious56 a65f291a89 Bonsai: gate clip-box refresh timer across file load
A pending RegionView3D.update() timer registered before wm.open_mainfile()
fires during the load against freshly-allocated regions whose GPU contexts
are not yet wired, CTD-ing inside GPU_matrix_ortho_set. Cancel both the
refresh and cap-rebuild timers in a new load_pre handler, hold a
_file_loading gate from load_pre through the first on_pre_view tick (first
paint = GPU ready), and short-circuit on_depsgraph_update during the
window so its IFC-reload schedule_refresh + apply_clip_planes_direct
branches can't re-arm against unready regions.

Generated with the assistance of an AI coding tool.
2026-06-30 10:23:06 +02:00
Thomas Krijnen e6dc582d82 ExtractElements - copy over coordionate operation #8199 2026-06-30 10:12:45 +02:00
Thomas Krijnen 366fa67a84 Don't rely on cwd 2026-06-30 10:02:55 +02:00
Petru Conduraru 4a8b863b96 test(ifcpatch): ExtractElements regression test for georeferencing loss (#8199)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 09:59:07 +02:00
Dion Moult 46a695266a ifcviewer-web: export HEAPU8 for heap-size diagnostics
Lets tooling read the wasm heap size (e.g. to verify a large sidecar
streams by byte range instead of loading whole, and to watch memory while
battle-testing big models over the network). Standard emscripten runtime
method, zero size cost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:26:12 +10:00
Dion Moult 9db42df81c ifcviewer-web: stop large-model network streaming thrash (grow before fetch)
Battle-testing real sidecars over HTTP Range exposed severe thrash: a
531 MB model re-fetched 2.25 GB (4×) and never converged — viewAll puts
the whole model in frustum, so every chunk wants to be resident, and the
web async path made it worse two ways:

  - A web load only consumes pool space when it COMPLETES (async), so the
    per-frame issuance over-committed the pool; completions then failed
    applyStreamedChunk on a full pool, the chunk re-candidated with no
    cooldown, and re-fetched every frame.
  - Pool growth is itself async on web (provisional sub-buffers validated
    off the JS event loop), so even fetched chunks failed to alloc until
    the pool caught up, and re-fetched.

Fix: gate web chunk issuance on VALIDATED free space + in-flight
reservation, and grow the pool BEFORE fetching:

  - streaming_web_inflight_bytes_ reserves each in-flight load's footprint
    so we never have more bytes in flight than the pool can place.
  - When a visible chunk doesn't fit validated free, don't fetch — call
    pool_.requestGrowth() (BufferPool: drives the async provisional grow
    without allocating) and short-back-off; the chunk is fetched once,
    after space exists. When the pool is saturated (model > GPU memory),
    long-cooldown so a never-fitting chunk isn't re-fetched. Gating before
    the evictor also kills phase-2 visible↔visible swap thrash.
  - On async load failure, cool down (short if the pool can still grow,
    long if saturated) instead of re-candidating next frame.

Result (manual battle tool, host.mjs + real files): 531 MB now loads
23/23 chunks, 322 MB loads 14/14 — resident climbs monotonically with
ZERO thrash warnings and a stable resident set, vs the old re-fetch loop.
The whole model resides on the GPU and stays. (Remaining ~3× ramp
over-fetch — per-chunk re-loads during the async-growth ramp + read
amplification from chunk byte-locality — is a separate efficiency
follow-up, not thrash.) 6/6 web smoke + 107/107 unit pass; desktop
unaffected (the gate is web-only; requestGrowth is a no-op wrapper there).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:26:12 +10:00
Dion Moult 2c5e2d1685 ifcviewer-web: fetch a chunk's vertex + index ranges concurrently
beginWebChunkLoad read the vertex ranges, then in the completion callback
read the index ranges, then applied — two serial round trips per chunk.
On a network that's the dominant per-chunk latency. Now both reads fire
at once and a small shared join (payloads + per-read done/ok flags) runs
the apply when the second lands, halving per-chunk RTT. Model re-lookup
still happens at apply time, so a resetScene mid-flight is dropped safely.

Per-chunk concurrency stacks with the existing across-chunk concurrency
(driveStreamingLoads issues several loads per frame); the browser caps
simultaneous connections per origin, so no explicit in-flight cap is
needed. 6/6 web smoke + 107/107 unit pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:50:37 +10:00
Dion Moult 23dc5dac48 ifcviewer-web: stream remote sidecars over HTTP Range (?model=URL)
Adds a network byte-source alongside the local Blob one. The async-chunk
infra is source-agnostic — only the two JS primitives knew it was a Blob —
so this generalises them and reuses everything else:

  - ifcvReadRangeInto: local → Blob.slice; remote → fetch() with a Range
    header (206). If a server ignores Range and returns 200, the requested
    window is sliced out so it still works (without the bandwidth saving).
  - ifcvFileSize: Blob size, or the URL's total length resolved up front.
  - ifcvBeginUrlSource: resolves total size (HEAD Content-Length, else a
    0-0 ranged GET's Content-Range) then fires _ifcv_source_ready.
  - The metadata bootstrap is extracted into a source-agnostic
    loadSidecarMetadataWeb(label); loadSidecarFromBlobWeb / FromUrlWeb are
    thin entries. streaming_from_blob → streaming_from_web (now covers both).

main_web exports load_sidecar_from_url_c(url); shell.html reads a
?model=URL query param and ccalls it once the app is live (same-origin
needs no CORS; cross-origin hosts must send CORS + Accept-Ranges).

Test: serve.mjs now answers HEAD + Range (206) and falls back to the
ifcviewer-web source dir for sample.ifcview (embedded in the wasm, not in
build-web). New smoke case loads ?model=/sample.ifcview and asserts it
renders via the Range path. 6/6 web smoke + 107/107 unit pass; desktop
unaffected (web-guarded; only the shared field rename touches it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 12:44:37 +10:00
Dion Moult bc31e91e35 ifcviewer-web: handle device loss so GPU pressure can't freeze the tab
Reported: open a model in the web viewer, then launch the desktop
BonsaiViewer and open another model — the browser tab freezes, and a
fresh web tab then fails with "RequestDevice failed: Not enough memory
left". Root cause is GPU-memory contention: two heavy GPU clients on one
GPU, and the desktop app's allocations starve the browser's WebGPU
process, which reclaims our device.

We can't conjure GPU memory, but we were amplifying the symptom: with no
device-lost handler, render() kept driving a dead device —
wgpuSurfaceGetCurrentTexture returns Lost every frame and the
reconfigure + requestFrame retry becomes a tight per-RAF loop that hangs
the tab. Now the web device descriptor wires a device-lost callback that
latches device_lost_ (ignoring the intentional Destroyed reason from our
own shutdown); render() bails while set, so the loop goes idle instead of
spinning, and the console logs guidance to reload. The fresh-tab
RequestDevice OOM is genuine GPU exhaustion — surfaced as before, now with
a clearer message.

Verified the lost callback doesn't disturb Dawn-web's RequestDevice (all
5 web smoke tests still init + pass); desktop unaffected (device_lost_
stays false). The real contention path can't be reproduced in the headless
harness, so the loss handler itself is covered by review, not a test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:24:56 +10:00
Dion Moult a6cfb2651a ifcviewer-web: render through an sRGB surface view (fix dark colors)
The fragment shader pre-decodes sRGB→linear to cancel the surface's
automatic linear→sRGB write encoding, so the final bytes match the GL
backend. That only holds when the render target is an sRGB format. On
desktop the surface's preferred format already is (e.g. BGRA8UnormSrgb),
but the browser canvas only offers plain BGRA8Unorm — so nothing
re-encoded and the whole image (background + models) rendered ~3× too
dark (authored bg 0.125,0.137,0.161 → ~32,35,41 collapsed to ~3,4,6).

Fix: when the surface format isn't sRGB, render through an sRGB *view* of
it — the standard WebGPU canvas pattern. surface_view_format_ is the sRGB
sibling of surface_format_ (unchanged when already sRGB, so desktop is a
no-op); configureSurface advertises it via viewFormats, the colour
pipelines (main, MSAA target, edge) target it, and render() creates the
surface view with it. The screenshot path still reads the base texture, so
its BGRA byte-order check stays on surface_format_.

Regression test: sample a 1x1 background pixel and assert it isn't crushed
dark (R,B > 20). Verified visually too — bg is now the correct dark
blue-gray and the cube is properly lit. 5/5 web smoke + 107/107 unit pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:08:30 +10:00
Dion Moult 3cc759d72b ifcviewer: fix blank-until-interaction stall on web (streaming settle burst)
On first web load the sample stayed blank until a click/drag, then popped
in. Root cause: the main draw + cull run before driveStreamingLoads in
render(), so a chunk that becomes resident there is only painted a frame
later. On desktop the streaming thread keeps inFlightApprox() > 0 during a
load, so the render loop keeps ticking and the next frame paints it. On web
the sync MEMFS / Blob load finishes instantly (inFlightApprox stays 0), so
the single post-load requestFrame fired once and the on-demand loop went
idle before the geometry was ever drawn — until some input re-armed it.

Fix: arm a bounded settle burst (kStreamingSettleFrames) whenever there's
streaming activity — a load this frame, work still queued, or a visible
chunk not yet resident — and bleed it down over the next few frames, each
requesting one more. Covers the cull→display latency under an on-demand
loop and still quiesces at idle (no busy-rendering). General, not web-only.

Regression test: the sample must render with NO pointer input — a centred
patch (the framed cube) differs from a corner patch (background); a blank
stall leaves both as background. Verified empirically with a no-interaction
probe (canvas went from a static blank hash to a stable rendered one).
107/107 unit tests pass; 4/4 web smoke tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:11:58 +10:00
Dion Moult 2b58d7be74 ifcviewer-web: smoke-test click-to-select highlight
Third Playwright case: click dead-centre on the framed sample, assert the
canvas changes (selection highlight rendered) with zero WebGPU errors. A
broken async pick would hang init or leave the canvas unchanged. All three
cases pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:06:28 +10:00
Dion Moult 094575fc19 ifcviewer-web: async object pick → click-to-select on web
Desktop pick reads the pick staging buffer back with a blocking
`while(!done) waitTickInstance()` spin. On web that spin is a no-op
(Asyncify is off) and hangs the JS event loop, so click-to-select was
dead on web. This adds an async sibling that uses the spontaneous
map-callback pattern (AllowSpontaneous + the browser microtask loop)
already proven by the HiZ readback — no blocking.

  - encodePickReadbackToStaging(x,y,want_normal): the pick-pass render +
    copy-texel-to-staging, extracted from pickObjectAt verbatim and shared
    by both readbacks (desktop sync path unchanged).
  - pickObjectAtAsync(x,y,cb) [web]: encode, then map the staging buffer
    with a spontaneous callback that delivers object_id to cb. One pick in
    flight at a time (a pick issued mid-map is dropped → cb(0)).
  - applyPickToSelection(id, add, remove): routes a pick result through the
    selection state machine (replace / Shift-add / Ctrl-remove / empty-click
    clear), mirroring the desktop ViewportWindow click semantics. selection_
    marks dirty so the next render's uploadSelectionFlagsIfDirty flushes the
    highlight.

main_web wires it: a left release under a 4px drag threshold (no orbit) is
a pick at down-position * devicePixelRatio, with Shift/Ctrl modifiers;
the result callback applies selection and requests a frame. Web + desktop
build clean; 107/107 unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:06:28 +10:00
Ryan Schultz db0bbf6b84 adding vscode workspaces to .gitignore 2026-06-29 14:29:17 -05:00
Gorgious56 a757641b84 Bonsai: derive prefs PropertyGroups from EDIT_TYPES
Collapse two parallel hand-maintained lists in the addon-preferences
PropertyGroups into derivations from `tool.Parametric.EDIT_TYPES`:

- GizmoPreferences: the 10 `<name>: BoolProperty` annotations now
  generated from the full EDIT_TYPES list.

- DefaultParameters: add `has_default_parameters` flag to
  ParametricObject (set True on door/window/stair/railing/roof);
  derive the 5 `<name>: PointerProperty(type=BIM<X>Properties)`
  annotations and collapse the 5 hand-written `draw_expandable_panel`
  blocks in `draw_default_parameters` into loops driven by the flag.

Existing `test_gizmo_preferences_field_per_registry_entry` pinned the
GizmoPreferences contract; new
`test_default_parameters_field_per_registry_entry_with_defaults`
pins the DefaultParameters contract (one-directional: flag=True
implies field present, flag=False allows absence).

Generated with the assistance of an AI coding tool.
2026-06-29 16:29:44 +02:00
Thomas Krijnen 8bd22178f7 OCC_VER Compatibility 2026-06-29 11:30:37 +02:00
Thomas Krijnen b4d7780e14 Use opencascade::handle for compatibility with earlier versions 2026-06-29 11:30:37 +02:00
Frozen Forest Reality Technologies 7c092db9e6 OCCT 8 Update Part 4
Fix For : ``C:\Program Files\OCCT\inc\NCollection_Sequence.hxx(45,18): error C2280: 'CSLib_Class2d::CSLib_Class2d(const CSLib_Class2d &)': attempting to reference a deleted function``.
2026-06-29 11:30:36 +02:00
Frozen Forest Reality Technologies 38e3bb0590 Boost 1.88 Update 2026-06-29 11:30:36 +02:00
Frozen Forest Reality Technologies 7f49c945b9 OCCT 8.0 Update Part 3 2026-06-29 11:30:36 +02:00
Frozen Forest Reality Technologies 81f71e6418 OCCT 8.0 Update Part 2 2026-06-29 11:30:36 +02:00
Frozen Forest Reality Technologies 6318610bdb OCCT Update to 8.0 Part 1 2026-06-29 11:30:36 +02:00
Gorgious56 82dd1d94de Bonsai: batch host recuts in array/opening paths
Refs gh#8088. Array regen + multi-opening drops fan out N+1 wall recuts
per operator (one per child filling deletion + the final mirror recut),
making CSG opening-subtraction O(N^2) for a linear UX action.

Introduces tool.Geometry.batch_host_recut() — a context manager that
coalesces switch_representation and bpy.ops.bim.update_representation
calls per voided element within one operator transaction. The drain
re-reads the active representation so the recut reflects current IFC.

Wraps 7 entry points (regenerate_array, RegenerateArray, RemoveArray,
AddOpening, RecalculateFill, CloneOpening, regenerate_from_type) and
rewires 7 leaf call sites in opening.py, void/operator.py, and
mirror_parent_void_fillings_to_children.

An AST forward-compat guard pins the rewire contract: no direct
switch_representation or bpy.ops.bim.update_representation in the
three target files outside the helper definitions.

A 16-child array regen now recuts the wall once instead of 17 times.
The CSG cost per recut is unchanged; only the count is reduced.

21 new tests across three lanes (helper unit, entry-point coalescing,
AST guard) — all green.

Generated with the assistance of an AI coding tool.
2026-06-29 10:35:01 +02:00
Gorgious56 4004344c20 Bonsai: TAB enters wall parametric edit
Mirror the icon-click entry into wall parametric edit on the TAB key.
The dispatch in Modifier.try_applying_edit_mode had no branch for fresh
LAYER2 walls, so TAB landed in item mode instead of the parametric
draft + gizmos. Add the missing entry leg of the toggle, placed after
the generic is_object_editing branch so the finish leg still fires
when a wall is already in edit mode.

Generated with the assistance of an AI coding tool.
2026-06-29 09:48:20 +02:00
Dion Moult e70c58fe99 ifcviewer-web: smoke-test the Blob.slice byte-range load path
Adds a second Playwright case that picks the sample sidecar through the
file input, waits for the C side to confirm the blob load (console),
then asserts an orbit drag changes the canvas with zero WebGPU errors.
This exercises the #88 path distinctly from the embedded MEMFS sample —
a broken metadata-head/tail or chunk range read renders blank and fails
the orbit-changed-canvas check. Both cases pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:44:02 +10:00
Dion Moult 584504dcdc ifcviewer-web: stream user sidecars via Blob.slice byte ranges (#88)
Picked files are no longer copied whole into the wasm heap. The browser
File object stays in JS (Module.__ifcvFile) and is read lazily through
Blob.slice byte ranges, so a 200-500 MB sidecar never enters wasm linear
memory — only chunk-sized slices do.

Mechanism (web-only, #if __EMSCRIPTEN__):

  - JS glue (EM_JS): ifcvFileSize + ifcvReadRangeInto — slice [off,off+n)
    of the File and copy it into a caller-provided heap pointer, then call
    back _ifcv_on_range_done. No malloc across the boundary; C pre-sizes
    the destination from the read plan.
  - webReadRangesAsync: reuses planSidecarReadRanges to coalesce a range
    set into Blob.slice reads (1 MB gap — each slice is an async hop),
    scatters them into a destination laid out in input order, and fires a
    continuation when the whole set lands. An in-flight map keyed by id
    survives unordered_map rehash (scratch buffers are heap-owned).
  - loadSidecarFromBlobWeb: async metadata load — head (16 B) -> index
    count -> tail-to-EOF -> parseSidecarHead/Tail -> applyCachedModel, then
    tags the model streaming_from_blob and frames it.
  - driveStreamingLoads: blob-sourced models route to beginWebChunkLoad
    (async vertex+index range reads -> applyStreamedChunk in the callback),
    holding is_loading until the bytes arrive. The embedded MEMFS sample
    keeps the synchronous fopen path.

shell.html stashes the File and calls _load_sidecar_from_blob_c instead of
FS.writeFile'ing the whole thing; EXPORTED_RUNTIME_METHODS=['FS'] dropped.
Desktop is untouched (the new members + driveStreamingLoads branch are all
emscripten-guarded). Web links clean; desktop rebuilds; 107/107 unit tests
pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:43:53 +10:00
Dion Moult a0ba3c0b98 ifcviewer: extract pure buffer-based sidecar parse + read-plan helpers
Splits the v13 metadata wire-format knowledge out of the FILE*-bound
streaming reader into pure, buffer-based functions so the web byte-range
path (#88) can reuse it without loading the whole sidecar into the wasm
heap:

  - parseSidecarHead  — validates the 16-byte head, yields num_vertex_bytes
  - parseSidecarTail  — parses meshes/instances/georef/elements/strings
                        from an in-memory tail buffer, bounds-checked
  - planSidecarReadRanges + SidecarReadPlan — the range-coalescing /
    scatter planner, promoted out of the anonymous namespace

readSidecarMetadataOnly and the range readers now call these; desktop
behaviour is unchanged (head + tail are small, the bulk is still skipped
via seek). The metadata tail is split from the head around the bulk
sections, so a blob-backed loader just slices those two regions and
hands the bytes to the same parsers.

Closes a coverage gap: StreamingLoader had no unit tests. Adds
test_streaming_loader.cpp (7 cases: metadata round-trip, corrupt/truncated
rejection, vertex+index range scatter, head validation, tail truncation,
read-plan coalescing). 107/107 unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 15:26:29 +10:00
Dion Moult fcf3a645db ifcviewer-web: add a headless-browser smoke test
Drives the built web page in a real Chrome (channel:'chrome', so no
`playwright install`): waits for wgpu init, then asserts an orbit drag
changes the composited canvas — one check that simultaneously proves the
scene rendered, mouse input is wired, and the log overlay isn't eating
events — and that zero uncaptured WebGPU errors were logged. Every web
bring-up bug so far (blank render, error-buffer cascade, overlay
swallowing input) is this shape; this would have caught them.

serve.mjs statically serves build-web; the config launches headed
against the real GPU (--use-angle=vulkan + --ignore-gpu-blocklist are
load-bearing for a non-null adapter on Linux Chrome). node_modules and
results are gitignored. See README.md to run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 12:14:27 +10:00
Dion Moult 89beee6514 ifcviewer: detect web pool-grow OOM via provisional sub-buffers
On web we skip the desktop error-scope spin-wait (it blocks the JS event
loop and hangs the page). The old web addSubBuffer then judged success by
`buf != nullptr` — but Dawn-web returns a NON-NULL error buffer on OOM,
so the pool committed an invalid sub-buffer, alloc handed out slices in
it, and every chunk_bind_group built against it failed ("BindGroup is
invalid" spam + a wgpuQueueSubmit panic). Loading a model larger than the
browser's WebGPU budget triggered exactly this.

Add the grown sub-buffer as *provisional* (alloc and the capacity/free
tallies skip it) and validate it through a non-blocking AllowSpontaneous
PopErrorScope. resolveProvisionalGrowth() clears the flag when it's good,
or drops the sub-buffer and latches growth_disabled_ on a real OOM — at
which point the streaming evictor bounds the working set to what fits
instead of cascading. Only one provisional grow is in flight at a time
(growth_pending_). Desktop keeps its synchronous halve-retry path
unchanged. All 100 unit tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 12:13:34 +10:00
Dion Moult 2237b4acdd ifcviewer-web: stop the log overlay from covering + blocking the canvas
The status overlay was position:fixed top/left/right with max-height
80vh and pointer-events:auto — a near-fullscreen div that both hid the
model and swallowed mouse events, so orbit drags over most of the canvas
did nothing. Move it to a small bottom-left box, set pointer-events:none
so it never intercepts navigation, and collapse it to a few dimmed lines
once the app goes live (errors re-expand it). It still auto-scrolls to
the newest line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:33:14 +10:00
Dion Moult e3722cf243 ifcviewer-web: load user sidecars via a file-browse button (#46)
Adds an "Open .ifcview…" button that opens the browser's native file
chooser. The picked file's bytes are written into MEMFS and a new
exported entry point, load_uploaded_model_c, reads them back via the
existing loadSidecarFromPath: it resetScene()s the current model
(replace, not append), loads the sidecar, and viewAll()s it. Geometry
becomes resident over subsequent frames through render()'s inline
driveStreamingLoads, same as the embedded sample.

Deliberately a file picker, not drag-drop: browser file drag-drop needs
an X11 drag source (a file manager) to drag *from*, which a minimal WM
(ratpoison) doesn't provide. The native chooser is WM-independent.

Exports _load_uploaded_model_c and the FS runtime method; URL/byte-range
fetch of remote models stays for #88.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:19:49 +10:00
Dion Moult d36d241da2 ifcviewer: size streaming pool to demand instead of probing for the max
The desktop pool walked down from 4 GB looking for the largest single
buffer the runtime would grant, then used that as the pool's first
sub-buffer and growth increment. On a stack that advertises an
effectively unbounded maxBufferSize (1 TB observed on wgpu-native here)
the walk lands on 2 GB, so loading even a 1.2 MB model allocated a 2 GB
sub-buffer. That plus the depth/MSAA/HiZ/pick attachments exhausted
VRAM, and the next tiny allocation — the ~4 KB selection_flags buffer —
failed with "Not enough memory left", invalidating its bind group and
panicking wgpuQueueSubmit.

Drop the probe and size the pool to demand, mirroring the web path:
configure a modest initial sub-buffer and let BufferPool::addSubBuffer
grow it lazily (halve-retrying to its 64 MB floor on constrained
devices). A single chunk is capped at 16 MB, so the initial sub-buffer
only has to clear that. Web keeps its 64 MB initial (Chrome contends on
large first allocations); desktop uses 256 MB to keep sub-buffer count
low for big models. The two platforms now share one createPool() (was
probeAndCreatePool — it no longer probes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:09:12 +10:00
Dion Moult c0c0c987c4 ifcviewer-web: wire mouse + wheel navigation (#85)
Register emscripten HTML5 pointer/wheel handlers that translate raw
events into ViewportCore::orbitBy / panBy / dollyBy: left-drag orbits,
middle/right-drag pans, wheel zooms. mousedown binds to the canvas;
mousemove/up bind to the window so a drag keeps tracking off-canvas. A
contextmenu suppressor lets right-drag pan without the browser menu.

Pure callbacks — no Asyncify, no sync spin — so none of the web init
gotchas apply. Pick stays deferred until the async buffer-readback
rewrite. The embedded sample is now navigable on Chrome + Firefox.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 10:50:11 +10:00
Dion Moult d92f6e6226 ifcviewer: route desktop orbit/pan/zoom through ViewportCore (#85)
Replace the inline orbit/pan/wheel math in ViewportWindow's mouse
handlers with calls to ViewportCore::orbitBy / panBy / dollyBy. The core
methods request the frame (via the host), so the now-redundant
requestUpdate() calls drop out; the pivot-indicator afterglow and 3px
drag-promotion stay in the Qt layer where they belong. Behaviour is
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 10:50:11 +10:00
Dion Moult 1d13b05acf ifcviewer: add incremental orbit/pan/zoom to ViewportCore (#85)
The orbit navigation math lived in the Qt ViewportWindow, operating on
ViewportCore's camera fields through references. That left the web host
with no way to drive the camera — orbit/pan/zoom were Qt-only.

Lift the three pixel-delta moves into ViewportCore as orbitBy / panBy /
dollyBy so every host (Qt desktop + web) shares one implementation and
the math can't drift between platforms. panBy takes the viewport height
as a parameter (the one Qt coupling: pan's world-units-per-pixel needs
it) so the core stays toolkit-free.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 10:49:54 +10:00
Petru Conduraru fce7cd3eb4 test(ifcpatch): add MergeProjects regression test for merging 3+ files (#7973)
Merging more than two IFC models with the MergeProjects recipe leaves
duplicated IfcGeometricRepresentationContext entities behind. All elements
are kept, but the accumulated contexts cause later disciplines to appear
"not merged" in viewers.

This test merges three projects and asserts the elements are kept, a single
IfcProject remains, and the geometric contexts are reused rather than
accumulated. It currently fails on the context assertion, reproducing #7973.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 13:43:07 +02:00
Thomas Krijnen f3dcf0b539 Attempt at less double counting of inverses #7973 2026-06-28 13:42:36 +02:00
Thomas Krijnen f5ebbd1917 Add test case for https://github.com/buildingSMART/validate/issues/305 2026-06-28 11:12:14 +02:00
Thomas Krijnen 77aa080334 Recompile rules 2026-06-28 10:37:33 +02:00
Thomas Krijnen 5afad2b179 Partial revert of 84abf5e91 2026-06-26 16:29:27 +02:00
Thomas Krijnen 98aa2c635e Rerun express-related codegen 2026-06-26 16:09:44 +02:00
Thomas Krijnen 7db1ceb07f Convert to lower() immediately after originalTextFor to retain more similar behaviour 2026-06-26 16:01:46 +02:00
Thomas Krijnen 84abf5e918 Adapt codegen for new parsing 2026-06-25 14:29:14 +02:00
Thomas Krijnen 741e8233ca originalTextFor() for on parsing express string literals 2026-06-25 14:29:14 +02:00
Thomas Krijnen 262b55630c Allow to compare simple type instance to underlying type 2026-06-25 14:29:14 +02:00
Gorgious56 f60a3423d0 Fix: commit pending parametric draft before extrusion-edit
EnableEditingExtrusionAxis and EnableEditingExtrusionProfile both
import a mesh from the IFC representation into obj.data as their
first real action. That mesh-import overwrites any in-memory
parametric (gizmo) draft on the object, silently discarding the
user's pending dimension edits.

Concrete reproduction: drag a wall's length gizmo (draft pending),
then click "Edit Axis" before validating the draft. The axis-edit
imports the wall axis mesh; the in-flight draft vanishes; on
cancel the wall snaps back to its pre-drag length.

Both call sites now commit the active draft via
tool.Parametric.commit_object_draft before the mesh import, gated
on tool.Parametric.is_object_editing so the guard is a no-op when
no draft is in flight.

Generated with the assistance of an AI coding tool.
2026-06-24 17:08:50 +02:00
Gorgious56 4f2c4d1633 Bonsai: promote load-warning banners to top-level UI
Multi-instance cache-lock, "Opening Cuts Skipped", and "Arrays With
Missing Children" banners now draw in BIM_PT_tabs alongside the
existing global error / outdated-model banners, so they remain
visible regardless of the active Bonsai tab. The corresponding
blocks are removed from BIM_PT_project. Dead imports
(is_cache_locked_by_other_process, draw_multiline_text) dropped
from project/ui.py.

Generated with the assistance of an AI coding tool.
2026-06-24 17:08:50 +02:00
Gorgious56 3b584ef242 Bonsai: short-circuit recreate_wall when no layer set
regenerate_wall_representation returns None for walls without an
IfcMaterialLayerSet (the only mode it knows how to rebuild). Feeding
None to switch_representation crashes deep in resolve_representation
on .Items. Document the None return on the API side and bail in
tool.Model.recreate_wall when it hits.

Generated with the assistance of an AI coding tool.
2026-06-24 17:08:50 +02:00
Gorgious56 10ee5aef4f Bonsai: refuse class-mismatched type assignment
Schema-illegal IfcDoor->IfcWallType pairings parse cleanly but propagate
into operators that fan out by type and eventually crash the wrapper.
Block the pairing at its source: API guard in ifcopenshell.api.type.
assign_type, per-object partition in BIM_OT_assign_type + DuplicateType,
new tool.Type.is_relating_type_compatible helper, AST forward-compat
guard. Files in the wild are still loaded unchanged.

Generated with the assistance of an AI coding tool.
2026-06-24 17:08:50 +02:00
Thomas Krijnen 59383f5010 constexpr more cases to prevent gcc calling non-existing template overloads 2026-06-24 11:34:04 +02:00
Thomas Krijnen a9d6776875 header.assign() helper 2026-06-24 11:12:21 +02:00
Thomas Krijnen 1057f794f6 Small conv result number tweaks 2026-06-24 11:12:11 +02:00
Thomas Krijnen b3218ced3f Mistake in parse examples include macro 2026-06-24 08:46:21 +02:00
Thomas Krijnen 49d9c416b1 Fix compilation 2026-06-23 21:51:27 +02:00
Thomas Krijnen 55e778f82e does_self_intersect() requires tri mesh 2026-06-23 20:26:28 +02:00
Thomas Krijnen 13681cdf9b Build all volumes when converting between nef and poly 2026-06-23 20:26:16 +02:00
Thomas Krijnen c592018b3f Minor changes to conversion result numbers 2026-06-23 20:25:57 +02:00
Thomas Krijnen dad4cc8a3c Merge branch 'v0.8.0' of https://github.com/IfcOpenShell/IfcOpenShell into v0.8.0 2026-06-23 20:23:13 +02:00
Gorgious56 006a24ef32 Refactor: rename rel -> subject in disconnect_rel dispatch
The "mep-pair-fitting" kind added in the previous commit carries an
IfcFlowFitting (the entity whose deletion disconnects the pair), not a
relationship entity, in the dispatch slot — but the slot was named ``rel``
across the function signature and every call site. Rename to ``subject``
so the parameter name reflects the uniform intent: "the entity whose
teardown effects the disconnect", regardless of whether that's a rel or
a fitting.

Sweep covers:

- core.connection.disconnect_rel signature + body
- tool.Connection.find_rels / find_rels_for_element / find_rel docstrings
- The cascade-on-delete call site in tool.Geometry.delete_ifc_object
- DisconnectElements operator in bim.module.model.wall
- All affected test kwargs and AST forward-compat docstring
- Error message: "Unknown rel kind" -> "Unknown kind"

No behaviour change.

Generated with the assistance of an AI coding tool.
2026-06-23 14:39:53 +02:00
Gorgious56 6fa984ce2b Fix MEP pair-disconnect crash and bend re-edit pen icon
Three user-facing fixes for the MEP-system gizmo surface:

1. MEP pair-disconnect no longer crashes Blender. The
   MEPSystemPathDecorator cached entity_instance references in
   _cached_walk; deleting a bridging fitting via the gizmo left a freed
   SWIG handle in the list, and the next _build_geometry pass segfaulted
   on .is_a. The cache now stores STEP integer ids and re-resolves via
   ifc_file.by_id on each draw, plus folds tool.Parametric.get_geom_generation
   into the cache key — ifcopenshell.api mutations invalidate before the
   next frame regardless of how the deletion was routed.

2. Bend re-edit pen icon stays reachable. The bend creation path
   tessellates the swept-disk body (upstream geometry-kernel workaround),
   so tool.System.has_parametric_body correctly returns False for a
   freshly-committed bend. _active_is_bend_fitting and
   GizmoMEPActions.is_eligible_object now fall back to the type's
   BBIM_Fitting pset — the same source bim.enable_bend_preview_from_bend
   reads parameters from — keeping the pen icon eligible.

3. MEP pair / per-port unjoin icons unified through bim.disconnect_elements.
   The MEP gizmo group's three unjoin icons (pair, start, end) now share
   the wall-disconnect surface: same VIEW3D_GT_wall_link_toggle icon, same
   bim.disconnect_elements operator. tool.Connection.find_rels learned a
   new "mep-pair-fitting" kind that returns the bridging fitting as the
   disconnect target; core.connection.disconnect_rel grew the matching
   dispatch arm. The old MEPUnjoinAtPort and MEPUnjoinPair operators are
   removed.

Also registered wall.GizmoPairDisconnect (previously declared but never
in the classes tuple, so dead code) for the wall+slab pair-disconnect
surface, and extracted MEP port-topology helpers (find_bridging_fitting,
is_disconnectable_fitting, neighbours_at_ports) onto tool.System so the
canonical walk has a single home.

Generated with the assistance of an AI coding tool.
2026-06-23 14:39:53 +02:00
Gorgious56 262f5f9a85 Merge pull request #8195 from Gorgious56/bonsai/ifc-migrate-ifc2x3-downgrade
Bonsai/ifc migrate ifc2x3 downgrade + patch preset system
2026-06-23 10:15:00 +02:00
Gorgious56 44c0c2916c Bonsai patch: lossy-downgrade popup + per-recipe preset menu
Two new UX features in the IFC Patch panel, both backed by helpers on
bonsai.tool.Patch.

Lossy-downgrade confirmation popup. When the user picks the Migrate
recipe with a target schema older than the source's (IFC4 -> IFC2X3,
IFC4X3 -> IFC2X3), ExecuteIfcPatch.invoke shows a properties dialog
listing what's preserved vs lost: IfcIndexedPolyCurve flattened with
arcs approximated, IfcPolygonalFaceSet / IfcTriangulatedFaceSet
converted to IfcFacetedBrep, IFC4-only IfcElement subclasses (IfcLamp,
IfcPipeSegment, IfcGeographicElement, ...) demoted to
IfcBuildingElementProxy with the original class + PredefinedType
encoded into ObjectType, and PredefinedType enum values absent from
IFC2X3 dropped. The user explicitly approves before the recipe runs.

The popup is gated on tool.Patch.migration_is_lossy_downgrade() which
resolves the source schema via header-only parsing
(tool.Patch._patch_source_schema reads the first ~2KB and matches a
FILE_SCHEMA regex, then normalises via ifcopenshell.util.schema.
get_fallback_schema). Avoids a full ifcopenshell.open() on every
Execute click — multi-second saving on large files. The target schema
is looked up by argument name rather than position so it survives
recipe-parameter reordering.

Per-recipe preset menu. New BIM_MT_ifc_patch_presets + AddIfcPatchPreset
wire Blender's standard preset system into the panel. Each recipe gets
its own preset subdirectory (bonsai/ifc_patch/<RecipeName>/), so a
preset saved for ExtractElements does not pollute the Migrate preset
list. The preset operator uses Attribute.get_value_name() (single
source of truth for data_type -> storage-field mapping) to build the
preset_values list dynamically per recipe.

The recipe-change callback resets
BIM_MT_ifc_patch_presets.bl_label to the canonical title — Blender's
script.execute_preset mutates the menu's bl_label to the loaded
preset's name as a "currently-selected" indicator, and without an
explicit reset the previous recipe's preset name would falsely advertise
itself in the new recipe's menu.

tool.Patch gains get_preset_subdir, migration_is_lossy_downgrade,
_patch_source_schema as cross-cutting helpers. _SCHEMA_AGE module
constant provides the ordering used by the downgrade-detection
predicate.

Test coverage: 12 bim-lane tests under test/bim/module/patch/. The
truth table for migration_is_lossy_downgrade covers IFC4/IFC4X3 source
x downgrade/upgrade/same-schema target x Migrate/non-Migrate recipe.
The schema-sniffing tests write a real IFC4X3_ADD2 file to disk and
assert the helper resolves it to IFC4X3 (regression for the original
startswith iteration-order bug). An end-to-end test drives
bpy.ops.bim.execute_ifc_patch with an in-memory IfcLamp source and
verifies the on-disk IFC2X3 file contains a single
IfcBuildingElementProxy with ObjectType "IfcLamp/COMPACTFLUORESCENT"
and the original GlobalId preserved.

Generated with the assistance of an AI coding tool.
2026-06-23 09:48:54 +02:00
Gorgious56 2ab5ca9222 ifcpatch: small recipe polish
ExtractElements: expand the `query` docstring to cover the exclusion
syntax (`!` on entity classes, `!=` on attribute / pset / material /
classification / location / group facets) and the "seed with a broad
include before subtracting" gotcha — entity-class exclusion does not
auto-seed from "all elements", so a bare `! IfcSlab` query returns
nothing.

FixArchiCADToRevitDoorSwings: guard the `IfcIndexedPolyCurve.Segments`
loop against the IFC4 case where Segments is absent (a polyline
through all coords in declared order). Previously crashed on
`None.__iter__`.

Generated with the assistance of an AI coding tool.
2026-06-23 09:46:31 +02:00
Gorgious56 f710929e9e ifcpatch Migrate: defensive IFC4/IFC4X3 -> IFC2X3 downgrade
The Migrate recipe previously crashed mid-loop with the cryptic
`RuntimeError: Entity with name '' not found in schema 'IFC2X3'` when
asked to downgrade an IFC4 or IFC4X3 file to IFC2X3 — the
class_4_to_2x3 mapping marks IFC4-only geometry / element classes with
an empty-string sentinel and the old code blindly forwarded that to
create_entity. Real files routinely contain IfcPolygonalFaceSet,
IfcTriangulatedFaceSet, IfcIndexedPolyCurve, IfcLamp, IfcPipeSegment,
IfcGeographicElement, etc.

The recipe now runs a preprocessing pipeline when the target is IFC2X3
and the source is IFC4 or IFC4X3:

- DowngradeIndexedPolyCurve flattens IfcIndexedPolyCurve to IfcPolyline
  for the whole file (arcs included — see below).
- IfcPolygonalFaceSet / IfcTriangulatedFaceSet are converted directly
  to IfcFacetedBrep at the entity level via
  ifcopenshell.util.shape_builder.polygonal_face_set_to_faceted_brep,
  preserving topology including IfcIndexedPolygonalFaceWithVoids inner
  bounds. IfcShapeRepresentation carriers have their RepresentationType
  tag updated from "Tessellation" to "Brep".
- Orphan source-only geometry instances (left over after the rewires)
  are purged iteratively via
  geometry_classes_introduced_after(target, source).

The Migrator is invoked with fallback_element_to_proxy=True so
IFC4-only IfcElement subclasses (IfcLamp, IfcPipeSegment,
IfcGeographicElement, ...) become IfcBuildingElementProxy in the
output. A post-pass encodes "<OriginalClass>/<PredefinedType>" into
ObjectType (e.g. "IfcLamp/COMPACTFLUORESCENT") when ObjectType is
empty, so the lost subclass identity survives the downgrade as
searchable text.

The migration loop now collects per-entity failures into a list rather
than crashing on the first; a summary RuntimeError fires at end if any
failed, naming up to 20 with their inverse references. Successful
migrations log a single count line via self.logger.

DowngradeIndexedPolyCurve extended:
- Arc segments (IfcArcIndex) are flattened via
  ifcopenshell.util.shape_builder.arc_to_polyline_points with
  ARC_SUBDIVISION=16 chord points per arc.
- Multi-index IfcLineIndex segments handled correctly.
- Absent Segments list (IFC4 polyline-through-all-coords case) handled.

Test coverage: 11 tests across the two recipes covering all four
preprocessing branches, the IFC4X3 source gate, the ObjectType
encoding (incl. author-supplied ObjectType preservation), the summary
RuntimeError shape, and the arc subdivision.

Generated with the assistance of an AI coding tool.
2026-06-23 09:33:03 +02:00
Gorgious56 a2dafc9ceb ifcopenshell.util: schema-aware downgrade helpers
Adds the IFC-library primitives the ifcpatch Migrate recipe needs for a
defensive IFC4 / IFC4X3 -> IFC2X3 downgrade without each caller
reinventing the wheel.

In ifcopenshell.util.schema:
- Migrator(fallback_element_to_proxy=False) opt-in: when True, IFC4-only
  IfcElement subclasses (IfcLamp, IfcPipeSegment, IfcGeographicElement,
  ...) migrate to IfcBuildingElementProxy instead of raising. Default
  preserves the strict failure-on-unmappable contract for existing
  callers (classification API, etc.).
- geometry_classes_introduced_after(target, source) derives the
  IfcRepresentationItem subclasses present in `source` but absent in
  `target` directly from the loaded schemas. Cached per pair. Replaces
  hand-curated class lists that drift with each IFC update.
  ifc4_only_geometry_classes() retained as an alias.
- generate_default_value synthesises a unit IfcAxis2Placement2D /
  IfcAxis2Placement3D when downgrading entities whose Position became
  required in the target schema (IfcIShapeProfileDef and friends in
  IFC2X3).
- Enum-mismatch detection upgraded from string-matched RuntimeError to a
  structural check via ifcopenshell.util.attribute.get_enum_items so
  upgrade paths still surface real bugs loudly.

In ifcopenshell.util.shape_builder:
- polygonal_face_set_to_faceted_brep converts IfcPolygonalFaceSet /
  IfcTriangulatedFaceSet (IFC4-only) directly to IfcFacetedBrep,
  preserving topology including IfcIndexedPolygonalFaceWithVoids inner
  bounds. Validates inputs at the boundary.
- arc_to_polyline_points approximates a circular arc through three
  points with a chord polyline of configurable subdivisions. Tolerates
  floating-point noise on planar Z. Raises on non-planar or invalid
  inputs.

Test coverage: 47 unit tests across schema + shape_builder lanes
covering each helper directly (no transitive-only coverage), including
regression pins for the IFC4X3-prefix ordering invariant in
get_fallback_schema and the strict-default Migrator contract.

Generated with the assistance of an AI coding tool.
2026-06-23 09:23:25 +02:00
Gorgious56 3c9ee4a71f Clip box: include linked IFC geometry
Add include_linked_ifc toggle on BIMSceneClipBoxProperties so the cap
pipeline can also bisect meshes inside Project > Links collection-instance
empties. Off by default - linked IFCs may carry the entire site or
structural backbone, and capping them adds per-mesh bisect cost on every
clip-box edit.

The new iterator composes instance.matrix_world @ inner.matrix_world as
the effective world placement so caps land in the active scene rather
than at the linked library's local origin. Linked-mesh cache entries are
namespaced with a 'link:' prefix to avoid collisions with top-level
scene objects.

Generated with the assistance of an AI coding tool.
2026-06-22 13:58:45 +02:00
Gorgious56 27b0b920a9 Apply black formatting and ruff isort fixes
Pre-commit checklist: black + ruff check.

Generated with the assistance of an AI coding tool.
2026-06-22 11:09:18 +02:00
Gorgious56 6367de6102 Add tool.Blender.draw_quads utility
Promotes the private _fill_quads_alpha helper from
bim/module/model/decorator.py to tool.Blender.draw_quads so any feature
decorator can reuse the same TRIS-batch path.

The new utility accepts an optional outline_color so callers can draw
fill, outline, or both in a single call. Migrates the only existing
caller (WallGizmoPreviewDecorator in model/wall.py) to the public API
and removes the local helper.

Generated with the assistance of an AI coding tool.
2026-06-22 11:09:18 +02:00
Gorgious56 3c20c27794 Extend clip box with face handles and presets
Add source-based clip box presets — a dropdown menu next to the Add
Clip Box button lets the user pre-size a clip box to the bounding box
of a chosen IFC source: a spatial element, IFC class, type, material,
profile, drawing camera frustum, status, system, group, or zone. The
picker dialog uses prop_with_search so files with hundreds of materials
or types remain browsable.

Add interactive face resize handles — six near-invisible click-target
quads render on the active clip box when its empty is the active
object. Dragging a face grows or shrinks the box one-sided on that
axis; the opposite face stays fixed. Ctrl+Click on a face aligns the
viewport to look at that face, following Blender's numpad-view
convention applied to the box's local axes so rotated boxes align
orthogonally to the screen. The gizmos honour negative-scale empties
so the visible cube and the clickable handles stay aligned.

Add settings and info menus — a gear-icon menu next to the Enable
Clipping / Show Caps toggles exposes per-file preferences (cap only
IFC products, show face handles); an info-icon menu adjacent documents
the gizmo gestures. A quick-access toggle row also appears in the
viewport Overlay popover, greyed out when no clip box exists, and
orphaned clip-box list entries now expose an X button so users can
recover from external host-empty deletions.

Plumbing: cap rebuild fires synchronously on gizmo release and
clip-box selection change, so the cross-section overlay re-forms
without waiting for the depsgraph debounce; cap eligibility honours
the "Only IFC Products" toggle. Includes 121 tests covering source
resolution, drag math, face visibility, gizmo registration, and the
view-alignment up-axis convention.

Generated with the assistance of an AI coding tool.
2026-06-22 11:09:18 +02:00
Thomas Krijnen 4f21bd1c69 Auto mem mngt in conversion result number types; more arithmetic on OpaqueCoordinate 2026-06-22 10:38:25 +02:00
Thomas Krijnen 312be203c9 SYN004 test case 2026-06-20 21:30:31 +02:00
Thomas Krijnen 7c9df9f980 Don't erronously terminate on [SYN004] Non-entity type messages 2026-06-20 21:18:23 +02:00
Thomas Krijnen 669e04664d Consistent policy on normalization in halfspace eq map() 2026-06-20 13:30:17 +02:00
Thomas Krijnen c0f2fb1860 Store actual subentities in CgalShape instead of the strange degeneracies convention 2026-06-19 20:50:40 +02:00
Thomas Krijnen 9396340b13 parse examples: Include -definitions.h header as well so that preprocessor switches works 2026-06-19 20:38:37 +02:00
Gorgious56 e2a4e5692f Add network path overlay for walls and MEP
Adds a viewport overlay that traces the connected element path from
the selected wall or MEP element. Walls follow IfcRelConnectsPathElements
and draw each connected wall's reference axis with endpoint dots; MEP
elements follow IfcRelConnectsPorts and draw each segment's axis plus
a port-to-port spider for each fitting.

The new BIMModelProperties.show_paths toggle (Element Paths in the
Bonsai Decorators group of Blender's viewport overlay popover) drives
install / uninstall of both decorators on flip and on file load,
mirroring the show_slab_direction wiring. The popover row also
surfaces the pre-existing BIMSystemProperties.should_draw_decorations
toggle (System Decorations) so both connectivity overlays sit
together.

Dot colors split free endpoints (decorator_color_special, blue by
default) from junction nodes (decorator_color_selected, green by
default) so dangling chain tips read apart from interior joins. Walls
classify endpoints by IFC topology: rels expose RelatingConnectionType /
RelatedConnectionType and ATPATH dots use tool.Wall.path_connection_location_world
for the canonical T-meets join. MEP keeps the geometric classifier
because port positions coincide exactly across fitting + segment.

Generated with the assistance of an AI coding tool.
2026-06-19 09:07:11 +02:00
Gorgious56 104eeaf0cd Fix clip-box edit-mode picker + rotation margin
Edit-mode click-select rejected verts inside the clip volume
because clip_bb stayed at the first-arm view; the depsgraph and
PRE_VIEW handlers updated clip_planes but skipped the
view3d.clip_border call that refreshes clip_bb. Schedule a full
re-arm at transform-commit, IFC reload, and view drift.

The empty's wireframe was clipped by its own planes when rotated
at non-trivial scale because the 1e-6 absolute margin can't
absorb float-precision drift that scales with the box's world
half-extent. Add a 1e-5 relative expand.

Generated with the assistance of an AI coding tool.
2026-06-18 17:39:25 +02:00
Gorgious56 260a387069 Fix loading project library without IfcProject
Per IFC4+, IfcContext is the abstract supertype of IfcProject and
IfcProjectLibrary; library-only files legitimately contain only
IfcProjectLibrary as their root context. Bonsai assumed an IfcProject
was always present at three crash sites: the parent-library enum
(reported in #8183), RefreshLibrary's tree view, and AddProjectLibrary.

Introduce tool.Project.get_root_context() that prefers IfcProject and
falls back to IfcProjectLibrary, and route the three sites through it.
get_parent_library() now returns None for a root IfcProjectLibrary;
get_project_hierarchy() and the EditProjectLibrary parent-swap branch
handle that. AddProjectLibrary creates the nested sub-library via
IfcRelNests when the root is an IfcProjectLibrary, matching the
existing convention for library-under-library nesting.

For the separate "Open IFC Project" path, abort with a friendly error
pointing users to Project Setup -> Project Library -> Select Library
File instead of letting set_units() crash deep in the importer.

Closes #8183.

Partly generated with the assistance of an AI coding tool.
2026-06-18 17:17:32 +02:00
Bruno Perdigão 937270fc49 Fix thickness and offset calculation for rotated slabs.
Get existing `x_angle` instead of using object `rotation_euler.x`

Co-Authored-By: Ryan Schultz <ryan@openingdesign.com>
2026-06-17 22:20:52 -03:00
Bruno Perdigão 156c6183eb fix custom offset unit scale when loading from pset. 2026-06-17 22:20:51 -03:00
Bruno Perdigão 95fcf9e35c fix custom_offset scale material layers 2026-06-17 22:20:51 -03:00
Thomas Krijnen 855de34d22 Catch and log errors during initialize_settings() 2026-06-17 18:23:19 +02:00
dependabot[bot] 1e91eebf51 Bump tar from 7.5.11 to 7.5.16 in /src/ifctester/webapp
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.11 to 7.5.16.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.11...v7.5.16)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.16
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-17 14:37:12 +02:00
Thomas Krijnen d5bed316cd Option for ifcwrap cmake to run standalone #8165 2026-06-17 14:28:36 +02:00
carlopav 4d3bff4e3a feat(ifc5d): include quantity Formula in serialised Quantities
IfcQuantity* carries an optional Formula (IfcLabel) documenting how a
quantity was derived. Export it alongside each quantity so it survives
in the Quantities column.

The per-quantity entry shape grows from [name, value] to
[name, value, formula], which stays backward compatible for positional
consumers reading index 0/1. Formula is read with a schema-safe getattr
(it does not exist on IfcPhysicalComplexQuantity, nor in IFC2X3) and is
coalesced to "" when absent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 13:34:46 +02:00
Gorgious56 0b0e34f18d Merge pull request #8178 from Gorgious56/bonsai/clip-box
Adds a viewport clip box feature to Bonsai: a user-controllable oriented bounding box that hides everything outside its 6 faces and draws filled cross-section caps where IFC product geometry intersects the planes.

Quality and coordination > Sandbox > Clip Box
2026-06-16 13:31:04 +02:00
Gorgious56 6147a58d7a Add viewport clip-box feature
A clip box hides everything outside a user-controllable oriented
bounding box, with cross-section caps drawn where IFC product
geometry intersects the planes. The box is hosted on a Blender
empty (CUBE display); its matrix_world is the single source of
truth — G/R/S edits the empty and the viewport clip planes track.

State persists through IFC save/load via a project-level pset
(IfcProject.BBIM_ClipBoxes) so the boxes survive without binding
to any IfcRoot entity (avoids the IFC scale-lock / strip).

UI: BIM_PT_clip_box under the Sandbox tab. Prominent Enable
Clipping + Show Caps toggles at top, then Add, then a UIList with
per-row duplicate / remove icons. Scene-level enabled / show_caps
so the "hide everything outside" intent applies file-wide;
enabled is intentionally not persisted to the pset so reopening
an IFC never silently hides geometry. Adding a clip box arms
clipping so the user immediately sees the cut.

Default spawn at the 3D cursor with scale 10 (a 20 m cube) so the
volume covers a typical building storey or two rather than the
meaningless 2 m unit cube.

Modal-aware: depsgraph + draw-handler paths gate per-frame side
effects on tool.Blender.is_transform_modal_active so dragging
G/R/S on the box only writes the pset once on commit, not per
frame. Shift+D / Alt+D / Ctrl+Shift+D on a clip box gets adopted
as a first-class entry via the collection-to-list sync.

Cap eligibility is gated on IfcElement (walls, slabs, doors, …)
so spatial structure (IfcSpace, IfcBuildingStorey, IfcSite) and
annotations / grids never sprout solid fills at clip boundaries.

Cap rebuild is debounced behind a 1 s quiet window so external
gizmo drags (and any other burst of non-Bonsai depsgraph updates)
collapse to one rebuild on release. Bonsai's own G/R/S keeps the
snappy on-release feel via a modal-end fast-path. The relevance
filter compares a per-Object matrix hash against a baseline so a
plain selection click — which Blender quirkily flags as a
transform update — doesn't churn the cache or flash the caps off.
Edit mode short-circuits both the rebuild scheduler and the draw
handler entirely.

Caps use the evaluated mesh (modifier stack applied) and a
session/matrix/clip-box-hash cache so a typical scene only
re-bisects meshes whose geometry actually changed.

Performance: every per-frame poller (refresh, depsgraph handlers,
draw handlers) short-circuits on the cheapest available check
first — cap_cache emptiness for the post-view draw handler,
scene_props.enabled for the rest — so a session with clipping
disabled pays only one boolean read per tick.

Known v1 limitations documented in tests / docstrings: hollow
profiles cap as solid discs (single-ring tessellation only),
non-watertight inputs may produce degenerate caps, quad-view
untested, Cycles / EEVEE render not supported (GPU-overlay only).

Generated with the assistance of an AI coding tool.
2026-06-16 13:26:44 +02:00
Gorgious56 51eb8aece7 Add bisect_and_cap helper to tool.Geometry
Bisects a BMesh against a set of planes (clear_outer per plane),
then fills the cut edges as cap faces tagged via a BMesh int layer
so the tag survives subsequent bisects. Cut edges are grouped into
connected components before filling so a hollow profile's outer +
inner loops produce two separate cap faces instead of a single
welded outer face that hides the hole. Pre-welds T-junctions
introduced by IFC Boolean meshes so the cut closes into a fillable
loop.

Callers are responsible for input mesh quality. Non-watertight
inputs (terrain, single-shell surfaces) may produce degenerate
cap faces — that's an accepted user-supplied data limitation
which can be revisited if real-world feedback shows it matters.

Used by the clip-box feature to compute cross-section caps per IFC
product mesh.

Generated with the assistance of an AI coding tool.
2026-06-16 13:26:08 +02:00
Gorgious56 5cc9daa2f9 Add OBB clip-plane and planar tessellation to tool.Cad
Adds geometry primitives the viewport clip-box feature needs:

- obb_world_clip_planes / obb_clip_planes_from_matrix: derive the 6
  inward clip planes of an oriented bounding box (or unit cube under
  a matrix_world) in RegionView3D.clip_planes form. expand / expand_rel
  margins let callers visualising the box with overlapping geometry
  (an empty CUBE display sharing edges with the planes) keep the box's
  own wireframe inside the clip volume.
- point_is_inside_clip_planes / corners_might_cross_clip_planes: cheap
  reject tests for the per-mesh capping pass to skip the expensive
  bisect when an object's AABB is fully outside the box.
- newell_normal / plane_basis: robust planar-ring normal for thin
  near-degenerate cap rings where a two-edge cross product is unstable.
- tessellate_ring_planar: triangulate [outer, *inners] 3D rings in the
  outer ring's best-fit plane, with a shapely constrained-Delaunay
  fallback for the known failure mode of mathutils.tessellate_polygon
  on complex concave polygons-with-holes.

Tests cover unit-box, translated, rotated, and scaled cases for the
OBB-from-matrix builder + the rejection helpers.

Generated with the assistance of an AI coding tool.
2026-06-16 13:24:42 +02:00
Gorgious56 a56b5660d0 Extract transform-modal gate + viewport helpers to tool.Blender
The transform-modal active check (Bonsai keymap macros + Blender's
TRANSFORM_OT_* family) was a module-local helper in drawing/gizmos.py
used by per-gizmo poll callbacks. It needs to be shared with other
features that gate per-frame side effects on whether a drag is in
progress (clip box plane re-arming, future modal-aware decorators).

Move BONSAI_TRANSFORM_MACROS and the gate into tool.Blender as
is_transform_modal_active classmethod; widen its window scan to all
WM windows for callers without a window-bound context (depsgraph
callbacks). Leave a thin module-local alias in drawing/gizmos.py so
AST scans and existing call sites stay decoupled from the helper's
home module.

Also add generic Blender helpers needed by the clip-box feature
(reusable by any future feature):

- iter_view3d_regions: yield (area, region, region_3d) for every
  WINDOW region in every 3D viewport — for clip-plane / draw-handler
  fanout.
- get_or_create_collection: idempotent named-collection lookup +
  link to a scene.
- is_in_edit_mode: True iff the active object is in any EDIT_*
  mode — for features that need to suspend per-tick work during
  vert/edge/face manipulation.
- serialize_matrix / deserialize_matrix / hash_matrix: round-trip a
  4x4 matrix as a 16-float CSV string for IFC pset persistence + a
  matching hash for cache keys.

Generated with the assistance of an AI coding tool.
2026-06-16 13:23:46 +02:00
Gorgious56 5f1efeffaf Tolerate stale array child/parent GUIDs (#8177)
* Tolerate stale array child/parent GUIDs

A real-world IFC project (an arrayed door whose host got deleted
externally) crashed Bonsai's project load with "Instance with
GlobalId not found" inside setup_arrays.

tool.Blender.get_object_from_guid declared Optional return but let
RuntimeError propagate; callers iterating BBIM_Array child lists then
crashed instead of skipping. Honour the documented contract by
returning None on miss, matching the convention used by every other
by_guid lookup helper in tool/array.py, tool/ifc.py, tool/geometry.py.

Sweep the four user-action sites that resolve array child/parent
GUIDs without a guard - they shared the same bug class but were
reachable from different operators (regenerate_array, RegenerateArray
clear, duplicate_ifc_objects, process_arrays). An already-missing
entity is the desired terminal state for each, so the fix is
try/except RuntimeError: continue/skip.

setup_arrays now also collects each parent with at least one stale
child GUID into IfcImporter.broken_arrays, surfaced via a new Project
panel banner mirroring the existing pending_opening_recut UX. The
banner reports the count and offers "Select Elements" to navigate to
the affected array parents and a Dismiss button.

constrain_children_to_parent was being called once per layer inside
setup_arrays' for loop even though it always iterates all layers
internally - lifted out of the loop (pre-existing N x perf bug
that the stale-GUID print exposed).

Regression tests:
- test_returns_none_when_guid_not_in_file pins the get_object_from_guid
  Optional contract.
- test_remove_array_tolerates_stale_child_guid injects a fake child
  GUID into BBIM_Array.Data and asserts bim.remove_array completes
  cleanly.

Generated with the assistance of an AI coding tool.

* Black: wrap long bl_description in dismiss_pending_array_repair

Generated with the assistance of an AI coding tool.
2026-06-16 10:33:56 +02:00
carlopav 074021de70 fix(ifc5d): escape quantity names when serialising Quantities to JSON
serialise_cost_quantities built the "Quantities" JSON string by manual
concatenation, inserting quantity.Name and the related element's Name
without any escaping. A name containing a double quote, backslash or
newline produced invalid JSON, breaking any downstream parser (e.g. a
Typst json.decode consumer reporting "failed to parse JSON"). It also
crashed with a TypeError when a name was None (str += None).

Build a Python list and serialise it with json.dumps instead, keeping
the exact same [[name, value], ...] output shape, the element-name
prefix and the unsupported-type behaviour. None names are coalesced to
"" and quantity values are defensively coerced to float.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 06:35:06 +02:00
Gorgious56 ed7239526c Merge pull request #8173 from Gorgious56/bonsai/wall-slab-gizmos
Bonsai/wall slab gizmos
2026-06-15 16:00:56 +02:00
Gorgious56 7cd7db0c2b Tidy: black formatting + PR7a test docstrings
Wraps three over-length lines black wanted on the merge-filter +
fillet-lock commit (wall.py's ``either_is_fillet`` chain rewraps the
right-hand ``or`` operand; test_disconnect_elements.py patch-stacks
break each ``patch(`` onto its own continuation line).

Adds per-test docstrings to test_wall_props_resync_on_dim_change.py
and test_wall_split_filled_opening.py so the contract each pins is
visible on grep / on test-run failure output without scrolling to
the module-level docstring. Drops a flip_object sibling-symbol
mention from the module docstring per CLAUDE.md §4a.

Generated with the assistance of an AI coding tool.
2026-06-15 14:56:46 +02:00
Gorgious56 13c89ace83 Fix merge crash + surface/lock fillet preview connections
DumbWallJoiner.merge previously crashed on walls with a slab underside
clip because the ConnectedTo / ConnectedFrom migration loops assumed
every rel was an IfcRelConnectsPathElements. The slab's
IfcRelConnectsElements(TOP) rel has no RelatingConnectionType /
RelatedConnectionType and raised AttributeError mid-migration. Filter
on rel class; the slab rel dies with element2 via the trailing
delete_ifc_object cascade.

The fillet preview pen icon now also flips the corner's
BIMWallProperties.is_editing so the connection-disconnect gizmos
surface in parallel with the radius drag. CancelWallFilletPreview
clears the flag before tearing the preview state down so both UIs
hide together. GizmoWallUnjoinSingle.poll inlines the viewport +
array-child guards from the topology gate so the gizmo can show
during preview — its own is_editing check is the real gate.

Fillet-to-source-wall path connection icons render in a muted gray
(LOCKED_COLOR) instead of the active disconnect tone, and the
bim.disconnect_elements operator early-returns with an INFO report
("Fillet wall path connections can't be unjoined — delete the fillet
wall element to remove the corner.") when either side resolves to a
fillet corner. The slab clip rel kind stays disconnect-able since
its identity is separate from the fillet's chord-axis reference.

Drive-by /improve polish on adjacent wall.py code: 3 comment tightenings
dropping sibling-symbol names + a defensive ``if opening.ObjectPlacement:``
guard in the merge opening migration matching the pattern used elsewhere
in the same file.

Generated with the assistance of an AI coding tool.
2026-06-15 14:32:29 +02:00
Gorgious56 6119f0045e Swap merge convention to active-is-survivor
bim.merge_wall now consumes the non-active selection into the active
one — matching Blender's OBJECT_OT_join (Ctrl+J) and MESH_OT_merge
"at last" convention. The wall the user clicks last absorbs the
other; users following Blender muscle-memory get the result they
expect. DumbWallJoiner.merge is already structurally asymmetric
(wall1 = survivor); only the caller in MergeWall._perform needed
flipping. Audit confirmed the previous call site was the sole
caller of DumbWallJoiner.merge in production code.

Drive-by tidies on adjacent code: collapse two over-length comprehensions
under black's 120-char budget, and switch ``any(True for _ in gen)`` to
``any(gen)`` since the iterable yields tuples that are always truthy.

Generated with the assistance of an AI coding tool.
2026-06-15 12:37:24 +02:00
Gorgious56 bbe437adc8 Fix wall-split filled-opening classification + void copy
Two bugs in DumbWallJoiner.split's filled-opening branch:

1. Side classification read filling_obj.matrix_world.translation —
   flip-fragile because flip_object rotates the filler 180° + translates
   so the bbox stays visually in place, moving the door origin to the
   opposite bbox corner. A flipped door centred over the cut could be
   classified on the wrong side. Switch to the opening's axis-projected
   midpoint, which the unfilled-opening loop already uses.

2. When the void straddles the cut and the filling moves to element2,
   the void copy for element1 was taken from the rebound new_opening
   whose PlacementRelTo had been swapped to element2 — the new void on
   element1 then sat in element2's local frame. Reorder so the copy
   reads from the original opening (still hosted by element1) before
   remove_feature destroys it.

Generated with the assistance of an AI coding tool.
2026-06-15 11:41:01 +02:00
Gorgious56 7dcf415ef1 Resync wall props after dimension mutation
ChangeExtrusionDepth, ChangeExtrusionXAngle, and ChangeLayerLength
mutate IFC extrusion / axis but never re-prime BIMWallProperties from
the post-mutation state. Gizmo icons that position from props.height
then sit at the pre-mutation elevation even though the wall mesh shows
the new one — visible asymmetry against the workspace header H field
which redraws live. Add the existing _resync_walls_after_mutation
call to each operator's epilogue. _maybe_resync_wall_props_from_ifc
already skips non-walls and walls in edit mode, so calling on the raw
selection list is safe.

Generated with the assistance of an AI coding tool.
2026-06-15 11:06:32 +02:00
Gorgious56 3f8d7165e5 Preserve openings on wall merge
DumbWallJoiner.merge cascade-deletes element2's HasOpenings via
delete_ifc_object, which previously dropped every IfcOpeningElement
(and any IfcDoor / IfcWindow filling) hosted by the discarded wall.
Re-host each void rel onto the survivor BEFORE the delete fires, and
re-apply the opening's captured world matrix via edit_object_placement
so the void doesn't drift when the two walls have different
placements — a PlacementRelTo swap alone would fail this when origins
differ along the shared axis.

Generated with the assistance of an AI coding tool.
2026-06-15 10:57:07 +02:00
Gorgious56 ca1e2165e8 Merge pull request #8172 from Gorgious56/bonsai/railing-edit-gizmos
Bonsai/railing edit gizmos
2026-06-15 10:23:16 +02:00
Gorgious56 2f067187ee Disable snap on schematic gizmos
Schematic dimensions float in billboarded viewport space; their labels
carry the value, not the bar length. Snapping the dragged tip to scene
vertices produces nonsensical value jumps when the mouse crosses
unrelated meshes. Add an opt-out flag on the parametric gizmo group
base and override it on the schematic base — every schematic subclass
inherits no-snap behaviour, and in-place parametric gizmos (door,
window, wall, stair, roof, mep) keep the existing Ctrl-toggleable
snap because the default stays True.

GizmoDimension.invoke also forces tool_settings.use_snap = False for
schematic gizmos so the header magnet visibly switches off for the
drag's duration. The existing exit path restores the user's previous
setting on release.

Generated with the assistance of an AI coding tool.
2026-06-15 10:08:50 +02:00
Thomas Krijnen 6a6756de66 Bump binary versions in makefiles; add backwards compatibility to logger usage in python #8167 2026-06-15 09:56:36 +02:00
Thomas Krijnen 22707fa534 Bring back multiple schema includes in IfcParseExamples.cpp 2026-06-14 21:02:35 +02:00
Thomas Krijnen 3e7b739d8d Don't rely on typeid() naming in VariantArray 2026-06-14 20:27:49 +02:00
Thomas Krijnen ca99ef3af7 More changes to pass around logger to parse-related calls 2026-06-14 14:49:14 +02:00
Gorgious56 55428a0878 Add wall regen helper, fillet underside, bug sweep
Wall body rebuild + slab underside re-clip are now unified behind
tool.Model.regenerate_wall and called from split / merge / extend
operators. Fillet corner walls accept extend-to-underside (poll +
operator partition switched to is_path_connectable_wall) and surface
the wall-unjoin gizmo without the parametric-edit gate, since
fillets cannot enter that lifecycle. DumbWallJoiner.split strips the
duplicate's inherited slab-trim booleans up front so wall2 lands at
the cut point. regenerate_fillet_corner_wall re-clips after the body
rewrite so a prior extend-to-slab survives neighbour recalcs.

Drive-by bug sweep: tuple typo in hotkey_S_G's IfcSpace check,
defensive .get() in draw_regen_operations for partial AuthoringData
loads, and a try/except in get_active_representation matching the
existing convention for stale mesh ifc_definition_ids after a
representation rebuild.

Tests cover the regenerate_wall branching, the get_active_representation
stale-id contract, and the GizmoWallExtendVertically fillet acceptance.

Generated with the assistance of an AI coding tool.
2026-06-14 10:56:07 +02:00
Ryan Schultz 682bd0a4f7 closes #6235 - Add copy toggle to CAD offset (#8168)
Add a "Copy" option to bim.cad_offset. When enabled (the
default) it offsets a new copy of the selected edges as
before; when disabled it moves the existing edges to the
offset location instead. The toggle is exposed in the CAD
tool's Offset panel and the operator redo panel.

Generated with the assistance of an AI coding tool.
2026-06-12 14:08:21 -05:00
Thomas Krijnen 671217d494 Commit remainder of fixes to IfcParseExamples 2026-06-12 12:13:35 +02:00
Thomas Krijnen dcebf23af8 Workaround for header construction order 2026-06-12 11:41:11 +02:00
Gorgious56 bb8681a954 Cascade connection cleanup on element delete
Deleting a slab that was connected to a wall via IfcRelConnectsElements(TOP)
left the wall holding orphan IfcBooleanResult items + a stale BBIM_Boolean
pset. The disconnect operator already runs the right cleanup; element delete
just never invoked it.

Extract the per-kind cleanup into core.connection.disconnect_rel so the
operator (bim.disconnect_elements) and a new cascade in
tool.Geometry.delete_ifc_object share one dispatch table. Adding a future
rel kind to tool.Connection.find_rels now flows into both call sites
automatically; an AST forward-compat guard enforces coverage.

Other adjustments:
- regenerate_wall_to_underside zero-slab branch now removes stale clip
  booleans instead of silently skipping, so disconnecting the last TOP
  slab also reverts the wall correctly.
- duplicate_ifc_objects (Shift+D) calls strip_underside_booleans on copied
  walls so the duplicate doesn't carry over the source's slab trim, then
  reloads the body representation when something was stripped so the
  viewport reflects the change without waiting on Shift+G.
- batch_being_deleted_ids threads through OverrideDelete so the cascade
  can suppress partner-side regenerate when both endpoints are queued for
  deletion in the same batch.

This file was generated with the assistance of an AI coding tool.
2026-06-12 11:11:07 +02:00
Gorgious56 c7d5d6c498 Gate slab disconnect gizmos behind parametric edit lifecycle
Wires slabs into the parametric edit framework (tool.Parametric
.EDIT_TYPES) so the wall-slab disconnect UI gets ESC handling, red
cancel icon, mutual exclusion with other parametric edits, and
per-feature gizmo prefs — all from BaseParametricGizmoGroup — without
duplicating the lifecycle.

Adds:
- ParametricObject("slab") registry entry + tool.Parametric.is_slab
  predicate (any IfcSlab).
- BIMSlabProperties with is_editing flag; PointerProperty wired by
  the framework's register_object_properties.
- bim.enable_editing_slab / bim.finish_editing_slab /
  bim.cancel_editing_slab operators on tool.Ifc.Operator so they
  flow through tool.Parametric.run_bim_op cleanly. No IFC mutation
  — slab edit is a pure UI gate; finish and cancel share the body.
- tool.Model.get_slab_props accessor.
- GizmoSlabEdition inheriting BaseParametricGizmoGroup with the
  pen / validate / cancel triad. is_element_type narrows to
  IfcSlab with at least one wall clipped to its underside.

The disconnect-icon group GizmoSlabUnjoinWalls polls behind
_slab_connection_gizmo_poll_gate(require_editing=True), which now
reads is_editing through tool.Model.get_slab_props.

Drops the standalone GizmoSlabConnectionAccess + the
setup_pen_cancel_icons helper added earlier in this branch — both
superseded by the framework integration.

Also folds in the wall + multi-slab gizmo polish requested live:
- Wall side: stack the per-slab unjoin icons vertically (up to 5)
  so multi-slab connections each get a distinct clickable icon;
  hover-highlight reveals which slab will disconnect.
- GizmoPairDisconnect activates when 2 elements with an
  IfcRelConnectsElements(TOP) rel are selected, with the icon at
  the wall-slab connection world anchor.
- Wall-slab anchor moved from slab clip Z to wall top +
  WALL_SLAB_CONNECTION_Z_CLEARANCE so the disconnect icon perches
  above the extend-vertical / slope gizmo instead of overlapping.
- Shared _resolve_active_partner_pair helper for 2-selection
  gizmos; _slab_connection_gizmo_poll_gate added to
  _REQUIRED_CALLEES + GizmoSlabEdition added to the AST
  forward-compat allowlist.

Build note: wall.py's DisconnectElements._perform imports
bonsai.core.connection.disconnect_rel — that core module is being
added in a parallel-session commit. Until that lands the addon
import will fail.

Generated with the assistance of an AI coding tool.
2026-06-12 11:05:02 +02:00
Thomas Krijnen 4d22a3fdb9 Enable retargeting of example schema 2026-06-12 11:04:18 +02:00
Gorgious56 a3593ed58b Unify wall disconnect ops via bim.disconnect_elements
Single generic dispatcher replaces UnjoinWallPathConnection +
DisconnectWallSlab. Takes two GlobalIds, looks up every supported
rel between them via tool.Connection.find_rels, dispatches the right
cleanup by rel kind:

- path (IfcRelConnectsPathElements): remove_connection on every rel
  in both orientations + recreate both walls + resync drafts.
- element-top (IfcRelConnectsElements with Description=="TOP"):
  disconnect_element + regenerate_wall_to_underside on the wall side
  via orient_element_top to recover which input is wall vs slab.
- element (other IfcRelConnectsElements): plain disconnect_element.

tool.Connection lands as a new tool module with two helpers:
- find_rels(a, b): every supported rel between two elements, walking
  both ConnectedTo + ConnectedFrom (catches both authoring
  orientations and dedups by id).
- find_rel(a, b): first-match convenience.
- orient_element_top(rel, a, b): recovers (wall, slab) from a TOP
  rel regardless of which input came first.

Updates GizmoWallUnjoinSingle to target bim.disconnect_elements with
both element_a_guid + element_b_guid pre-filled per icon. Adds the
single registration in tool/__init__.py and the classes-tuple entry
in bim/module/model/__init__.py. Drops the two retired classes.

Tests cover both cleanup branches (path + element-top), missing
endpoints, no-rel-found, and registration smoke.

Generated with the assistance of an AI coding tool.
2026-06-12 09:06:09 +02:00
Gorgious56 b0eb55cc38 Add bim.disconnect_wall_slab operator
Counterpart to UnjoinWallPathConnection on the wall-slab side: takes
a wall + slab GlobalId pair, locates the IfcRelConnectsElements(TOP)
between them via tool.Wall.find_wall_slab_rel, removes it via
ifcopenshell.api.geometry.disconnect_element, then re-runs
core.regenerate_wall_to_underside so the wall re-clips against any
remaining connected slabs (the disconnected slab is excluded
naturally because the helper walks tool.Model.get_connected_slab_objs
which filters by the rel set).

Defensive reports replace silent CANCELLED on three error paths the
UI can hit when the gizmo dispatches against stale state: unknown
GlobalIds, wall entity without a Blender object, no rel found
between the resolved pair.

Tests cover all four control flows (happy path + three error paths)
plus a registration smoke that catches a forgotten classes-tuple
update.

A follow-up commit will retrofit this + UnjoinWallPathConnection +
the MEP port disconnects through a unified bim.disconnect_elements
dispatcher with a small connection-type registry; that lands as a
separate single-concern commit so the typed operator can be
reviewed first.

Generated with the assistance of an AI coding tool.
2026-06-12 08:19:04 +02:00
Gorgious56 f6590d8be2 Add tool.Wall slab-connection helpers + tests
Four classmethods enable the new wall-slab connection gizmo work:

- iter_wall_slab_connections(wall): yields (slab, rel) tuples for
  every IfcRelConnectsElements(TOP) on wall.ConnectedFrom — the rel
  kind extend_walls_to_underside creates.
- iter_slab_wall_connections(slab): mirror, walks slab.ConnectedTo
  so a slab-side gizmo can enumerate every wall clipped to its
  underside.
- find_wall_slab_rel(wall, slab): locates the specific rel between
  a wall + slab pair so a disconnect operator knows what to remove.
- wall_slab_connection_location_world(wall_obj, slab_obj): returns
  the world-space icon anchor — wall axis midpoint X/Y lifted to
  the slab's mesh-bbox underside Z. Approximate (uses slab bbox vs
  reconstructing the slab's clip plane) but adequate for icon
  placement on a wall whose top meets the slab; returns None when
  the wall has no IFC Axis representation.

Tests (11) pin the rel-shape contract (class + Description=="TOP",
non-TOP and non-IfcRelConnectsElements rels skipped, None relating
defensively skipped) plus the icon-anchor math (axis-mid lifted to
slab-bbox bottom; None for axisless walls).

Generated with the assistance of an AI coding tool.
2026-06-12 08:02:50 +02:00
Thomas Krijnen 3136c74c2f Pass logger to proj callback 2026-06-11 21:57:42 +02:00
Thomas Krijnen 347a3c80bb More logger changes 2026-06-11 21:09:56 +02:00
Thomas Krijnen df59c888fd Fixes after merge 2026-06-11 21:02:03 +02:00
Gorgious56 92aa890add Refresh railing preview on every gizmo edit
update_railing skipped the bmesh rebuild for WALL_MOUNTED_HANDRAIL
railings because the only mesh source available at the time mutated
IFC. The viewport-only preview helper that lands with the parametric
gizmo work (generate_wall_mounted_handrail_preview) sidesteps IFC
entirely, so the WALL_MOUNTED_HANDRAIL branch can join the
FRAMELESS_PANEL path and trigger update_railing_modifier_bmesh on
every property write. Gizmo drag now repaints the viewport in real
time instead of waiting for Finish Editing.

Generated with the assistance of an AI coding tool.
2026-06-11 20:49:41 +02:00
Gorgious56 05c9df74f9 Migrate railing terminal type to PickType menu
Switches the IfcRailingType terminal-type selector from cycle-on-click
to a popup menu of all terminal-type literals — 5+ values trip the
§2.8 menu-pick threshold. Updates classes registration; removes
EditRailingTerminalType in favour of PickRailingTerminalType which
inherits PickTypeMixin.

Adapts the cherry-pick from db016d881 to post-PR5 framework state:
- Imports CycleTypeMixin / PickTypeMixin / PathPreservingEditMixin
  from bim.parametric_lifecycle (PR5 moved them off gizmos.py).
- Routes is_railing through tool.Parametric (predicates moved off
  tool.Blender.Modifier between PR3-PR5).

Skips the parametric_lifecycle.py framework refactor the source
commit shipped — HEAD has the more-evolved post-PR5 framework that
already covers it.

Adds the _FakePropsBase + make_lifecycle_obj test helpers to
test/bim/conftest.py so the new test_railing_lifecycle.py can
exercise the edit triad without a real bpy.types.Object. Brings the
test_railing_schematic.py marker in line with the rest of the model
lane.

Generated with the assistance of an AI coding tool.
2026-06-11 20:49:27 +02:00
Gorgious56 964fb045f7 Guard HasShapeAspects access on IFC2X3 representation iteration
IFC2X3 representations have no HasShapeAspects inverse; opening the
Geometry & Materials subpanel on an IFC2X3 object raised AttributeError
and left the items list empty. Wrap the access with a getattr default
so pre-IFC4 schemas return an empty iterable, and pin the contract with
an AST forward-compat guard that scans bim/, tool/, and core/ for any
future direct .HasShapeAspects access.

Closes #8157

Generated with the assistance of an AI coding tool.
2026-06-11 18:49:50 +02:00
Gorgious56 08a3a3864b Fix np_frombuffer_legacy length-vs-dtype check
The check `len(bytedata) == n * 2` was wrong: float64 is 8 bytes per
element, not 2. Legacy float64 checksums fell through to the float32
reader and produced a (2n,)-shaped array, breaking is_moved() and
is_camera_moved() with `ValueError: operands could not be broadcast`
on .blend files saved by Blender <5.0.

Adds a parametrized regression test covering both n=3 (translation)
and n=9 (rotation) for both dtypes.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:21 +02:00
Gorgious56 37e080c6de Read wall extent from bbox in cursor gizmo layout
GizmoWallEdition.position_gizmos used props.anchor_x / props.length
for the in-range check (split icon visibility) and perpendicular
gizmo placement. Those props mirror IFC and are re-primed by
_maybe_resync_wall_props_from_ifc — any operator path that skips
the re-sync leaves the perpendicular gizmo clamped to the previous
wall extent, so the icon parks at the old wall end instead of the
cursor's orthogonal projection. Visible after a wall mutation as
the perpendicular icon landing way off the cursor in top-down view.

Switch to the mesh bbox along local X. recreate_wall rebuilds the
mesh to match the current IFC body on every wall mutation, so
bound_box is authoritative without an explicit props sync.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:21 +02:00
Gorgious56 903baa9e5c Hide MEP gizmos on non-parametric elements
MEP elements imported as tessellation / brep (no IfcExtrudedAreaSolid
or IfcSweptDiskSolid in their body representation) can't be
parametrically edited — the gizmos offer affordances the geometry
kernel has no path to honour. tool.System.has_parametric_body
inspects the Model/Body/MODEL_VIEW representation and returns True
only when at least one item resolves to one of the two
profile-sweep primitives.

The gate is wired into:
- GizmoMEPActions.is_eligible_object (the action icon group)
- _active_is_flow_segment / _active_is_bend_fitting visibility
  predicates the icon row consults per-icon
- GizmoPipeSegmentEdition / GizmoDuctSegmentEdition is_element_type

tool.Parametric.is_pipe_segment / is_duct_segment stay IFC-class-only
so their truth-table contract test keeps reading a single concern.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:21 +02:00
Gorgious56 0b95688dd1 Move _is_multiple_of_pi to tool.Cad
Pure-math parallelism check (value ≡ 0 mod π within VTX_PRECISION)
that lived as a module-private helper in mep.py belongs next to
tool.Cad.is_x — same comparator family, no MEP-specific knowledge.
Other features with rotation-difference checks (wall fillet, roof
slope, railing terminus) now have a sanctioned spelling.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:21 +02:00
Gorgious56 da95be801c Add MEP cache + smoke + cancel-ops forward-compat tests
Four standalone test files pinning contracts the production code
already honours:

- test_mep_actions_cache.py: GizmoMEPActions visibility-predicate
  cache evicts on selection or generation change.
- test_mep_bend_preview_cache.py: bend decorator polyline cache
  re-uses within a generation and rebuilds on generation bump.
- test_mep_distribution_fit_smoke.py: bim.fit_flow_segments
  round-trips a 3-segment polyline without raising.
- test_preview_cancel_ops_forward_compat.py: AST scan ensures every
  preview Enable* operator has a paired Cancel* operator with the
  matching prop reset.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:21 +02:00
Gorgious56 b6e03574d2 DRY transform-modal draw gate + polyline helper
Two small refactors:

- apply_transform_modal_draw_gate(group, context) replaces the
  three-line _is_transform_modal_active + _hide_all_non_modal_gizmos
  pair that BillboardingGizmoGroupMixin, BaseParametricGizmoGroup
  and BaseSchematicGizmoGroup all repeat in draw_prepare.
- decorator.py renames _stroke_lines_alpha to a public-scope
  draw_polyline_segments and drops the no-longer-private companion
  docstring reference; the function is now usable by sibling
  decorators that draw polyline overlays.

Plus a few one-liner tweaks in tool/model.py and opening.py
following the helper rename.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:21 +02:00
Gorgious56 083022dea3 Cache array-child + wall topology by IFC generation
Two hot paths the gizmo polls fire every viewport event memoise
their result against tool.Parametric.get_geom_generation():

- tool.Blender.Modifier.any_selected_array_child caches the
  per-selection scan against the selection identity-set + the
  IFC generation token so a stable selection during a drag
  doesn't re-walk every selected object's BBIM_Array pset every
  frame.
- bim/module/model/wall.py grows a pair-predicate + connection
  cache that the wall topology gizmos hit; both keyed on
  (pair_uids, predicate_kind, generation) so a wall split or
  axis edit invalidates correctly via the generation bump.

Behavioural contract is unchanged — stale entries are evicted
on generation bump; cache miss returns the same value the
un-cached path returned.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:21 +02:00
Gorgious56 a9512492f0 Centralise model test fixtures via conftest
bim/module/model/conftest.py exposes the autouse _require_real_bpy
skip-guard, four make_* factories (obj / element / context /
ifc_file), and a patched_tool context-manager factory that wires
the half-dozen tool.* boundary patches every gizmo + decorator
test was repeating.

Existing test files in the directory drop their local copies of
_require_real_bpy and adopt the patched_tool / make_* fixtures
where the call site simplifies — test_mep_port_operators.py is
the biggest beneficiary (−89 LOC).

No production behaviour change.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:21 +02:00
Gorgious56 9e35db593e Extract MEP bend preview + refine port operators
Three concerns bundled by file boundary (all in mep.py):

- Extract bend preview operators + GizmoBendPreview into a focused
  mep_bend_preview.py module; preview_base.py grows the shared helper
  set both bend and other previews now consume; classes tuple in
  model/__init__.py updated to register the new module.
- Surface ERROR reports on five silent CANCELLED returns in
  MEPUnjoinAtPort / MEPRemoveTerminalFitting / MEPUnjoinPair so a
  degenerate IFC file ("fitting has no Blender object", "connected
  port leads nowhere") shows up in the popup instead of looking like
  a no-op.
- DRY: _resolve_active_mep_segment + _require_port_state factor the
  segment-id-or-active-object resolve + port-state guard out of every
  port operator's prologue; _wire_anchored_icon_targets pulls the
  GizmoMEPActions setup() body into an exercise-without-MRO helper so
  the wiring-contract tests can hit it without instantiating the
  GizmoGroup.

Drops the now-unused preview_base import that the extraction left
behind.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:21 +02:00
Gorgious56 38ea9c0ac3 Brighten and dash opening occlusion outline
The opening preview's outline used a single-batch two-pass scheme that
dimmed the occluded back pass via alpha=0.25. The visible front pass also
inherited the source decorator color's modest alpha, so the outline read
as subtle on both sides.

Replace with a CAD hidden-line convention: solid full-alpha front pass on
the visible side, world-space dashed back pass on the occluded side. Both
passes use POLYLINE_UNIFORM_COLOR so depth and line-weight paths match.
The dashed batch is built once per object epoch by a new pure helper
tool.Blender.build_dashed_line_segments (pre-segments edges into world-
space dash chunks), then cached via the existing batch-cache mechanism
under "<uid>_dashed".

The solid front pass is rendered at a slightly wider line width than the
dashed back pass so its halo overpowers Blender's WIRE-display overlay
bias at outline pixels — without the asymmetry the wire's anti-z-fight
forward bias makes the LESS_EQUAL comparison narrowly fail and the
dashed pass wins on visible edges too.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:21 +02:00
Gorgious56 d83e643780 Migrate MEPConnectElements args from object names to IFC GUIDs
MEPConnectElements took obj1_name/obj2_name (Blender object names),
which break when objects are renamed or replicated by array
duplication. Switch to obj1_guid/obj2_guid resolved via
ifc_file.by_guid, with by_guid RuntimeError surfaced as an operator
error rather than a stack trace. DrawPolylineProfile (the sole
in-tree caller) updates to pass GlobalIds.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:21 +02:00
Gorgious56 3d8469a46f Add MEP bend tessellation helper tests
Pins the geometry contracts the hand-meshed bend body relies on
while IfcSweptDiskSolid round-trip is broken upstream (#8106):

- profile cross-section sampling: circle returns 16 evenly-spaced
  points starting at (radius, 0); rectangle returns the four
  canonical corners; anything else returns None so the rep swap
  is skipped rather than meshed against the wrong section
- parallel-transport framing keeps the cross-section continuous
  around L-shaped corners — pinned via start / end ring planes
- initial_basis override seeds the first ring with the source
  segment's local +X / +Y axes, fixing the asymmetric-rectangle
  twist the world-Z seed produces

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:20 +02:00
Gorgious56 8bc87dbde6 Add MEP port operator dispatch tests
Pins which IFC mutation each port operator commits and which
inputs each refuses with CANCELLED:
- MEPUnjoinAtPort removes the fitting + reconnects the two free
  ports; refuses if the named port is free or terminal
- MEPRemoveTerminalFitting deletes the terminal element + leaves
  the segment's port free; refuses on bridged fittings
- SelectMEPPathMembers walks IfcRelConnectsPorts in both
  directions from the active segment and selects every fitting /
  segment reachable through the port graph

Boundary mocks for tool.Ifc, tool.System and MEPGenerator stand
in for the IFC fixture; tests assert against the recorded
ifcopenshell.api.* calls.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:20 +02:00
Gorgious56 f71240bc5f Add GizmoMEPActions wiring contract tests
Pins two regressions the live MEP gizmo group can hit:
- per-icon setup() must write `position` (and `mode` on open-lock
  icons) onto every target_set_operator result; the test stands in
  for the AttributeError on bim.mep_add_obstruction that surfaced
  when a field was dropped from the operator declaration
- each visibility_condition lambda must stay total against None /
  non-IFC inputs, since a single raising predicate silently disables
  every sibling icon in the group

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:20 +02:00
Gorgious56 ab2fddb170 Fix decorator face-tri overlay artifacts
ProfileDecorator.draw_faces (used by the roof path-edit overlay) and
SystemDecorator.draw_faces called bmesh.ops.triangulate on the live
bmesh — both mutated the input and produced ear-clip fans that rendered
as visible streaks across n-gon roof faces at alpha 0.1. The opening
DecorationsHandler edit-mode branch had a separate bug: it computed
triangles from obj.data.calc_loop_triangles() while iterating the
edit-mode bmesh, so any topology added mid-edit desynced the indices.

Centralise the correct draw path on tool.Blender.draw_bmesh_face_tris
(wraps bm.calc_loop_triangles, non-mutating, beauty triangulator) and
route all three call-sites through it. A forward-compat AST guard walks
every *Decorator / DecorationsHandler class under bim/module/ and pins
the no-bmesh.ops.triangulate rule against future regressions.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:20 +02:00
Gorgious56 0319763376 Warn on shared-rep parametric edits
A user clicking the pen icon on a typed-product occurrence whose body
representation is mapped from its type would silently mutate every
sibling occurrence's geometry. Add a confirmation dialog at the pen-icon
dispatcher (the single chokepoint every feature routes through) showing
the sibling count, with a session-scoped suppress checkbox.

The check is read-only: tool.Model.get_sibling_occurrence_count wraps
tool.Geometry.get_elements_by_representation against the resolved body
rep and subtracts self + type. A forward-compat AST guard pins the
dispatcher monopoly so any future feature that binds pen_gizmo directly
to a feature-specific enable op fails the test before merge.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:20 +02:00
Gorgious56 83d4283934 Add bend re-edit gizmo
Once a bend was created, the only way to retune start_length /
end_length / radius was to delete and recreate from scratch.
EnableBendPreviewFromBend re-opens the preview on an existing
parametric bend: it walks the bend's ports to resolve the two
connected segments, reads start / end length and radius from the
bend type's BBIM_Fitting pset, and sets editing_bend_id on the
preview props. MEPAddBend then deletes the old bend + its port
connections (single undo step) before the recreate path runs, so
finish replaces the bend in place and cancel discards the edit
without touching the original.

GizmoMEPActions surfaces a pen icon on single bend-fitting
selections via the new _active_is_bend_fitting predicate; the icon
dispatches the new operator. Mirror of the wall fillet re-edit
flow (EnableWallFilletPreviewFromCorner + editing_corner_id in
CreateWallFillet).

Test coverage: registration probe for the new operator, an attached
editing_bend_id field probe on the preview umbrella, and a
parametrized truth-table for the _is_bend_fitting predicate
(IfcFlowFitting with BEND PredefinedType, with other PredefinedType,
with no type, IfcFlowSegment, IfcWall, None).

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:20 +02:00
Gorgious56 8efb5ae515 Hide wall topology gizmos on array children
Wall topology mutations (merge / join / extend-to-wall / unjoin /
fillet) applied to a Bonsai array child are silently overwritten by
the next ``regenerate_array``; merge also orphans a GUID listed in
the parent's ``BBIM_Array.Data``. Add a central
``tool.Blender.Modifier.any_selected_is_array_child`` predicate and
gate the five wall topology gizmo groups plus the six bound operators
behind it. Operator gating is defence in depth against keymap / F3
invocation paths that bypass the gizmo.

The base ``_wall_gizmo_poll_gate`` keeps its loose two-check shape
(viewport gizmos + no preview). A new
``_wall_topology_gizmo_poll_gate`` wraps it with the array-child
filter and is what the topology gizmos use. Host-opening gizmos
deliberately stay on the loose gate: openings authored on a child
are preserved through ``regenerate_array`` and track with the
replicated instance.

A forward-compat AST guard walks wall.py for ``GizmoGroup`` subclasses
and asserts each routes its poll through the tighter gate or the
central predicate, with an allow-list for the parametric-edit and
preview-owner exceptions. New wall topology gizmos inherit the
contract by construction.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:20 +02:00
Gorgious56 2c3935bf9d Add readonly door swing arc preview
Selecting a Bonsai-parametric IfcDoor now shows the swing arc(s)
without entering edit mode. A new viewport decorator polls on the
active object, reads the door's BBIM_Door pset, and draws the same
arcs the parametric door swing gizmo would draw — matching the
hinge / panel-width / x-mirror contract minus the is_editing gate.

A forward-compat test walks every door operation type and cross-
checks the readonly decorator's arc selection against the gizmo's
swing-arc config table, so future enum additions fail in both
surfaces simultaneously.

Also disables the inherited 8-pass dark halo on GizmoArc: an open
curve has no enclosed silhouette, so the offset passes read as
ghost arcs rather than a uniform outline. The arc's own cross-
section thickness keeps it legible without the halo.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:20 +02:00
Gorgious56 4a6087699b Add MEP bend preview + bend tessellation fallback
The MEP bend feature's IfcSweptDiskSolid representation produces
geometrically correct output but fails to round-trip through the
OpenCascade geometry kernel (upstream issue #8106) — the body is
dropped on the next file load. Until upstream is fixed, MEPAddBend
captures the bend centerline in world space before the segments are
extended (otherwise the post-extension axes no longer reach the
original intersection and arc reconstruction is wrong), then after
the fitting is placed it hand-meshes the bend body and swaps the
type's swept-disk representation for an IfcTessellatedFaceSet via
tool.Geometry.export_mesh_to_tessellation + tool.Model.
replace_object_ifc_representation.

The centerline includes the straight start_length / end_length legs
in addition to the arc so the bend covers the full segment-to-
segment span. Sweep uses parallel-transport framing — each ring's
(right, up) basis is rotated by the minimum rotation that maps the
previous tangent to the current one, eliminating the twist a fixed
world-axis reference produces when the tangent crosses the
reference. Cross-section orientation seeds from the source segment's
matrix_world local +X / +Y so asymmetric IfcRectangleProfileDef
ducts land with XDim / YDim on the same axes the segment expects;
parallel transport then preserves that alignment around the arc.
Centerline radius is radius + profile_dim[lateral_axis] to match
MEPAddBend's ref_point_radius — without this offset, the bend legs
fall short of the extended segments by profile_dim * tan(angle/2).
Face winding is left to the caller to correct via
bmesh.ops.recalc_face_normals on the closed bend tube.

Two FIXME(#8106) markers (capture site + helper call site) so both
can be dropped once upstream lands a swept-disk round-trip fix.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:20 +02:00
Gorgious56 122f6069f1 Add cursor-bound perpendicular wall gizmo
GizmoWallEdition gains a fourth cursor-anchored icon that
spawns a perpendicular branch wall from the cursor's
orthogonal projection on the source wall axis. Click forms
a T-junction; shift+click forms an L-corner with the source
wall trimmed at the projection, keeping its longer portion.

The branch inherits the source's spatial container and
centerline baseline so its authored axis matches the source's
alignment rather than the type's default.

Also includes a floor-plane preview quad for the new gizmo,
a floor-Z cross line on the split preview for top-down
visibility, a small bump to QUAD_ALPHA for clearer preview
fills, and a stacking-offset helper that centralises the
cursor-row screen-up step across three call sites.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:20 +02:00
Gorgious56 da36e5f7fc Add GizmoMEPActions + bend precondition + obstruction modes
The MEP one-shot operators (join, unjoin variants, terminal removal,
path-select, obstruction add/remove) had no viewport surface. This
commit adds GizmoMEPActions — the icon-action gizmo group that
surfaces them as billboarded icons around selected MEP elements.
Three anchor regions: a horizontal row above the bbox top
(selection-cardinality icons), per-port endpoints for the three-state
lock / unjoin icons (open lock for PORT_FREE, closed for
PORT_TERMINAL, unjoin for PORT_JOINED — resolved per-frame from
port_connection_state), and the predicted join location
(compute_mep_join_location, shared with the bend preview) for the
join / unjoin_pair pair. Unjoin icons render at full
DEFAULT_BILLBOARD_SCALE with warning-red hover; endpoint lock icons
shrink so the lock row stays subordinate to the row icons. The
group hides itself entirely while a bend preview is active.

MEPAddObstruction grew a position enum (CURSOR / START / END) and a
mode enum (ADD / REMOVE / TOGGLE) so the gizmo can target a specific
port without touching the cursor and dispatch ADD or REMOVE based on
the click target — the lock_open icons drive ADD with position
pinned, the lock_closed icons drive bim.mep_remove_terminal_fitting.
Without the new fields the gizmo wiring (op_props.position = ...)
crashed at setup() with AttributeError on the obstruction operator.

validate_bend_preconditions extracts the type-match and profile-kind
checks MEPAddBend enforces so EnableBendPreview surfaces the
rejection immediately — the user no longer tunes a preview only to
learn at commit time that the segments use an unsupported profile
(e.g. IfcArbitraryClosedProfileDef).

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:20 +02:00
Gorgious56 ccc4b4fc58 Add MEP unjoin / terminal-remove / path-select operators
Four discrete one-shot operators driven by the MEP segment's port
state. mep_unjoin_at_port deletes the IfcFlowFitting bridging a
segment's named port to a second element when the port is in the
JOINED state. mep_remove_terminal_fitting deletes the terminal
fitting at a port (closed-lock state) and dispatches by fitting
type — OBSTRUCTION fittings go through MEPGenerator.remove_obstruction
so the segment absorbs the freed length, other terminal fittings go
through the standard delete path. mep_unjoin_pair finds the single
fitting bridging two selected MEP segments and deletes it.
select_mep_path_members walks the connected MEP network from the
active element via IfcRelConnectsPorts and replaces the selection
with every reachable member. Foundation for the MEP Actions gizmo
group which surfaces these operators as icon affordances around
selected segments.

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:20 +02:00
Gorgious56 8c732ac3fa Align extend gizmo arrow with segment axis
The extend icon used a pure screen-space billboard that always
pointed +X across the screen — the arrow ran horizontally
regardless of the pipe / duct's orientation. The new
billboarded_along_axis helper rotates the gizmo about the camera-
forward axis so its local +X aligns with the segment's local +Z
projected onto the screen, keeping the icon camera-facing but
visually following the extrusion direction. The flip-mirror branch
now reads from cursor-vs-current-end along the segment axis (not
screen-X), so the arrow points away from the current endpoint
regardless of viewport orientation. The split icon stacks
perpendicular to the rotated extend arrow in screen space so the
two don't overlap.

The decorator's green preview line no longer clamps the cursor
projection to min_projected_length — it follows the raw projection
so the line stays visible when the cursor crosses behind the
segment origin (the user still sees where they're pointing even
though the operator floors the actual commit).

Generated with the assistance of an AI coding tool.
2026-06-11 18:46:20 +02:00
Gorgious56 04d0246910 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-11 18:46:19 +02:00
Gorgious56 92172880a4 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-11 18:46:19 +02:00
Gorgious56 8374dd6d46 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-11 18:46:19 +02:00
Gorgious56 db27d0ec0a 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-11 18:46:19 +02:00
Gorgious56 bfda77e3e8 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-11 18:46:19 +02:00
Gorgious56 2ac65aeb8b 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-11 18:46:19 +02:00
Gorgious56 0fc5c97eea 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-11 18:46:19 +02:00
Gorgious56 adf2603c0e 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-11 18:46:19 +02:00
Thomas Krijnen 0132ba39bb Catch decomposition errors #8149 2026-06-11 18:46:18 +02:00
Ryan Schultz 3cb20035ae Error on tessellation request in IFC2X3
IfcTriangulatedFaceSet/IfcPolygonalFaceSet were introduced in
Fix #7992: IFC4 and do not exist in IFC2X3. Previously, requesting an
IfcTessellatedFaceSet representation in an IFC2X3 file silently
fell back to a faceted brep after unassigning material sets.
Add a guard in the update_representation operator (user-facing
error) and in the add_representation API (ValueError) so the
unsupported request is caught instead of failing silently.

Generated with the assistance of an AI coding tool.
2026-06-11 18:45:18 +02:00
Gorgious56 af9c60f07b 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-11 18:45:18 +02:00
Bruno Perdigão 10c63b894d Add no headless test for Bonsai Snap Target. 2026-06-11 18:45:18 +02:00
Bruno Postle 3f9b25d4e0 Use version preprocessor guards for RocksDB unique_ptr API, retain unique_ptr internally 2026-06-11 18:45:18 +02:00
Bruno Postle 818ca6b2bc 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-11 18:43:10 +02:00
Bruno Postle 5f7d9b86b8 Use std::lexicographical_compare in Point_d_4d_Less 2026-06-11 18:41:08 +02:00
Bruno Postle 2dff2cd3b2 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-11 18:41:08 +02:00
Bruno Postle 78697582f7 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-11 18:41:07 +02:00
Thomas Krijnen 6f93357ed2 Fix --convert-back-units on transformation object #8137 2026-06-11 18:38:54 +02:00
Thomas Krijnen 9ffc505ab4 Check for empty result after BOPAlgo_MakerVolume and reset manifoldness state #8140 2026-06-11 18:38:54 +02:00
Thomas Krijnen 9ec05a37d1 Make faceset duplicate loop detection respect inner/outer #8140 2026-06-11 18:37:27 +02:00
Thomas Krijnen 093fd0e273 Re-sew non-manifold operands; interior loop re-orientations affect edge identity #8140 2026-06-11 18:37:27 +02:00
Thomas Krijnen 0eedc7bdc2 Sane error messages for unsupported items in geometry libs #8106 2026-06-11 18:35:59 +02:00
Gorgious56 99d758a330 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-11 18:34:46 +02:00
Gorgious56 b873db11db 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-11 18:34:46 +02:00
Gorgious56 882eff0b7e 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-11 18:34:46 +02:00
Gorgious56 e3738999c1 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-11 18:34:45 +02:00
Gorgious56 6391dbebb5 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-11 18:34:45 +02:00
Gorgious56 f5cdf5777e 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-11 18:34:45 +02:00
Gorgious56 54c00f0306 Fix demo preset crash + scope header refresh
bpy.ops.bim.new_project(preset='demo') crashed in
refresh_bim_tool_headers: the post-commit hook fired for every
nested bpy.ops.bim.append_library_element during template
loading, and the operator context Blender hands to
programmatically-invoked nested operators is stripped of the
view-layer attributes the refresh reads.

Two changes resolve it.

Gate the header refresh in tool.Parametric.refresh_post_commit
on operator.bl_idname being one of the EDIT_TYPES finish_op
idnames. Only validate-gizmo commits (bim.finish_editing_<name>)
now trigger the refresh; demo-loader and other non-edit
operators skip it. Querying the registry directly is the
canonical signal — string-prefix matching would silently drift
if ParametricObject.finish_op changes derivation.

Harden tool.Blender.get_active_object so its view_layer fallback
also uses getattr; the 150+ callers routed through it now
tolerate stripped contexts. _resolve_bim_tool_context applies
the same defensive pattern to mode / workspace.

Tests:
- test_handler_restricted_context covers get_active_object's
  defensive path and the BimTool-family whitelist (excludes
  annotation, spatial, structural).
- test_handler_forward_compat AST-pins that the gate consults
  EDIT_TYPES (not a string prefix).
- test_wall_header_refresh rewritten — three tests cover the
  gated-by-registry contract: counter bumps for every commit,
  finish_op operators refresh headers, others don't.

Hotkey-driven in-place edits (S_E / C_E) no longer trigger the
refresh — they were caught by the pre-refactor "every commit"
design. Left out of scope; the new skip-non-finish test pins
this as intentional.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:45 +02:00
Gorgious56 6f036edf08 Drop dead Geometry.has_material_styles + sanitation sweep
Two related cleanups bundled because each was too small on its own.

== Drop dead Geometry.has_material_styles duplicate ==

Two parallel has_material_styles implementations existed on HEAD:

* Geometry.has_material_styles (tool/geometry.py:853, added by
  3483683cb "Add tool.Geometry helpers for body representation +
  placement"): checks each material via tool.Material.get_style
  for an IfcSurfaceStyle. This is the implementation gizmos-8088
  uses — its core/root.py:58 calls geometry.has_material_styles.

* Root.has_material_styles (tool/root.py:75, added by e76455913
  "Route _has_material_styles through tool.Root.has_material_styles"):
  checks each material for a HasRepresentation inverse. Added to
  fix the test/core/test_root.py::TestCopyClass::test_AAAAAAAAAAAA
  failure by routing the check through a Prophecy-mockable seam.

HEAD's core/root.py:59 calls root.has_material_styles. The Geometry
version became orphaned by that migration — zero callers historically
(git log -S "Geometry.has_material_styles" returns nothing). The
Root placement is the right architectural home: has_material_styles
pairs with assign_body_styles in the copy_class flow as "is there
material-defined styling? if not, apply body styling" — both
decisions live on the same interface, called in sequence from the
same caller.

The semantic delta (HasRepresentation vs IfcSurfaceStyle) is a close
approximation in real IFC files where HasRepresentation almost always
indicates a styled material; if precision becomes necessary, the
Root impl can be tightened independently of this cleanup.

Drop the Geometry method + its abstract declaration in core/tool.py.

== Sanitation sweep per CLAUDE.md §4a ==

Eight rot-prone references in code we authored on this branch get
their first-draft mistakes cleaned up. The §4a rule (no sibling
symbol names, no test paths, no motivation history in docstrings)
got added during this branch, so older commits sometimes named their
siblings in prose; this is a focused cleanup of the worst offenders.

* bim/module/model/wall.py:201 — _CommitWallDraftsFirstMixin
  docstring carried motivation history ("...that every multi-wall
  operator … used to repeat at the top of _execute"). Rewrite to
  describe only the current contract.

* bim/module/model/wall.py:1910 — cycle_type_operator comment named
  two sibling methods. Rephrase to describe what happens at the slot.

* bim/module/model/wall.py:2025 — _active_instances ClassVar comment
  named WallGizmoPreviewDecorator. Rephrase to "the wall-gizmo
  preview decorator" (role, not class).

* bim/module/drawing/gizmos.py:3402 — GizmoFillet hit_uses_bbox
  comment named GizmoWallJoinIntersection. Rephrase to "the wall-join
  gizmo group".

* bim/module/drawing/gizmos.py:3887 — GizmoCountLabel docstring had
  a :meth:`set_count` cross-reference. Drop — reader sees the method
  next to the class.

* bim/module/model/host_add_opening_gizmo.py:201 — poll-exclusion
  comment named GizmoWallEdition + GizmoRoofEdition. Rephrase to
  describe why we skip ("walls and parametric roofs both render
  their own toggle in the pen row").

* bim/module/void/operator.py:45 — preserve_placement comment named
  FilledOpeningGenerator.generate. Rephrase to "the filling-opening
  generator gates its snap-to-wall-axis block on this flag".

* bim/parametric_lifecycle.py:64 — module docstring named the test
  file path (test/bim/test_parametric_registry.py). Rewrite to
  "enforced by the registry contract tests".

Sweep otherwise clean: no third-party software names in this-branch-
authored comments (upstream Revit / Tekla / ArchiCAD references are
legitimate external-constraint workarounds, §4a-allowed). No
PR/issue numbers we authored except the FIXME(PR5) in
tool/parametric.py:150, deliberately preserved until PR6's MEP slice
resolves it.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:45 +02:00
Gorgious56 a3533bfa49 Adopt _CommitWallDraftsFirstMixin on 7 wall operators
The 7 multi-wall operators (UnjoinWalls, UnjoinWallPathConnection,
ExtendWallsToUnderside, ExtendWallsToWall, SplitWall, MergeWall,
JoinWallsIntersection) each opened their _execute with an identical
prologue:

    _commit_pending_wall_edits_for_selection(context)
    # ... operator-specific logic

— flushing any in-progress wall parametric drafts so the operator
acts on committed IFC state rather than the draft preview box.

Extract that prologue into _CommitWallDraftsFirstMixin: its _execute
calls the commit helper, then delegates to a subclass-supplied
_perform. Subclasses inherit the mixin first in their bases tuple so
the mixin's _execute resolves first via the MRO. The IFC transaction
opened by tool.Ifc.Operator.execute still wraps both the commit and
the perform.

Behaviour-equivalent — same call, same order, same selection scope.
Architectural cleanup only: a future multi-wall operator can no
longer forget the commit step. The named helper
_commit_pending_wall_edits_for_selection stays as the single
encapsulation of the names=("wall",) filter; its docstring loses
the stale "every multi-wall operator calls it at the top of
_execute" sentence and now just describes the filter contract.

Matches gizmos-8088's _CommitWallDraftsFirstMixin pattern.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:45 +02:00
Gorgious56 e0aa39068d Shift-click add-opening preserves filling placement
The regular bim.add_opening click on the host-add-opening gizmo
(wall + door/window co-selected) routes through
FilledOpeningGenerator.generate, which snaps the filling to the
wall's reference-line axis, optionally rotates 180° when the
filling sits on the opposite side, and re-applies an rl1 / rl2
Z-elevation default. That is the right default for "drag a fresh
door onto a wall and let the model place it for me", but defeats
the workflow where the user has already positioned the filling
precisely (e.g. snapped to a window in an adjacent wall, copy-
pasted at an exact Z, aligned to a reference object).

Holding SHIFT while clicking the gizmo now opts into a
"preserve placement" mode: the filling stays at its current
matrix_world and the opening is created at the filling's existing
position. The opening / filling rels and representation work are
unchanged — only the snap-to-axis branch is skipped, so the IFC
graph is identical to the regular click; only the spatial
position of the filling differs (user-chosen vs auto-snapped).

Implementation:

* bim/module/void/operator.py: AddOpening gains a hidden
  preserve_placement BoolProperty + an invoke() that sets it from
  event.shift. The call into FilledOpeningGenerator.generate
  forwards the flag. bl_description documents the SHIFT modifier
  so it surfaces in F3 search / hover tooltip.

* bim/module/model/opening.py: FilledOpeningGenerator.generate
  accepts preserve_placement (default False — backwards-compatible
  with the other caller, tool.Model.add_filled_opening). The
  voided_obj.data-gated snap block (raycast + axis projection +
  rl-Z default + filling_obj.matrix_world write) skips entirely
  when the flag is True. The opening's matrix_world reads from
  filling_obj.matrix_world below the gate, so the opening lands
  at the filling's preserved position automatically.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:45 +02:00
Gorgious56 81bacb5899 Use menu pick gizmo for door / window / stair type
The door / window / stair edit-row's type-cycle icon advanced one
type per click (CycleDoorType / CycleWindowType / CycleStairType
bound to cycle_type_operator). DoorType has 8 IFC variants,
WindowType 9, StairType 3 — so cycling past the target was the norm.

Threshold rule for cycle-vs-menu: cycle is appropriate for exactly 2
values (advance-one-per-click stays predictable). Three or more
values warrants a popup menu. Door / window / stair all qualify;
roof (RoofGenerationMethod has 2 values) keeps cycle. Wall has no
type cycle. Array is unaffected.

Swap to the popup-menu pattern (PickTypeMixin already on HEAD at
bim/parametric_lifecycle.py:442): clicking the icon opens a menu
listing all type_literal values; selecting one applies it in a
single undo step. The hamburger icon (VIEW3D_GT_menu) is wired into
BaseParametricGizmoGroup.setup_editing_gizmos whenever
pick_type_operator is set (mutually exclusive with
cycle_type_operator). Matches gizmos-8088's pattern exactly.

Per-feature shape:

* door.py: PickDoorType replaces CycleDoorType.
  GizmoDoorEdition.cycle_type_operator → pick_type_operator.
* window.py: PickWindowType replaces CycleWindowType. Same swap.
* stair.py: PickStairType replaces CycleStairType (no
  tool.Ifc.Operator inheritance — stair-type changes
  BIMStairProperties only, no IFC mutation). Same swap.
* bim/module/model/__init__.py: registration entries renamed
  Cycle* → Pick*.
* bim/module/drawing/gizmos.py: drop the
  CycleTypeMixin / PickTypeMixin / TypeAccessorBase shim re-export —
  its own docstring already noted "PR5 cleanup drops these" and the
  three callers (door / window / stair Cycle*Type) it served are
  gone. Roof's CycleTypeMixin import was already direct from
  bim.parametric_lifecycle. Also update GizmoMenu docstring to
  reflect the 2-vs-3+ threshold.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:44 +02:00
Gorgious56 56097694bf Add host-wall offset gizmos for door/window edit
When entering parametric edit on a door or window that fills a
wall opening, four dimension gizmos now measure the distances
from the wall edges to the filling's jambs and from the wall's
base/top to the sill/header. Dragging any gizmo translates the
filling along the wall's local axis; 180°-flipped fillings and
slanted LAYER2 walls round-trip correctly. The has_host_wall
predicate hides all four when the filling → opening → wall
chain cannot be resolved.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:44 +02:00
Gorgious56 d0334a72ab Show wall cursor gizmos outside edit mode + axis previews
Four concerns that together make the cursor-anchored gizmos
(extend_x_gizmo, extend_z_gizmo, split_gizmo on GizmoWallEdition)
fully functional and visually informative without entering parametric
edit mode first:

* Drop the props.is_editing gate in _update_cursor_gizmos. The three
  bound operators (bim.extend_wall_to_cursor,
  bim.extend_wall_height_to_cursor, bim.split_wall_at_cursor) already
  poll on wall-selected and commit any pending wall edit before
  acting, so single-click without entering edit mode is now the
  canonical flow. Matches gizmos-8088's always-on behaviour.

* Register GizmoWallEdition instances in a per-region weakref map
  (_active_instances) populated at setup_element_specific_gizmos
  time. The WallGizmoPreviewDecorator dereferences this map to read
  live is_highlight state off the cursor icons. Without the
  registration its _cursor_icon_hovered always returned False and
  the hover-gated GPU previews silently never drew. Mirrors the
  same pattern already in place on GizmoWallJoinIntersection.

* Add post-operator resync to all three cursor operators
  (_maybe_resync_wall_props_from_ifc for the single-wall split /
  extend-height paths, _resync_walls_after_mutation for the
  selection-wide extend-X path). Without this, props.length /
  props.height stayed stale after the operator ran, so the
  orientation flips _apply_wall_extend_flips computes from
  cursor_local vs wall dimensions kept using the pre-extend values
  until the next selection change. Matches gizmos-8088's pattern.

* Hover-gated GPU previews per icon:

  - extend-X: filled Z=0 floor quads spanning the wall's offset to
    offset+thickness Y band, visible from plan view without side-
    view clutter. Grow case (cursor beyond either endpoint): one
    green decorator_color_selected quad over the extension. Shrink
    case (cursor inside extent): green quad for the portion that
    REMAINS + red decorator_color_error quad for the portion the
    operator REMOVES.

  - extend-Z: vertical lines at the cursor's projected X in the
    wall's y=0 reference-line plane. Grow case (cursor above wall
    top): one green segment from z=height to z=cursor.z. Shrink
    case: green from z=0 to z=cursor.z (REMAINS) + red from
    z=cursor.z to z=height (REMOVES).

  - split: one red vertical line at the cursor's projected X from
    base to wall top — the cut plane.

  Quads use QUAD_ALPHA=0.25 so the underlying wall body stays
  visible.

* New module-level _fill_quads_alpha helper next to
  _stroke_lines_alpha, plus a per-decorator _fill convenience method
  and a _wall_floor_quad corner builder.

Modal-active gizmo hiding (is_gizmo_hidden_by_modal) is preserved.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:44 +02:00
Gorgious56 046917f75d Generalise opening gizmos + DRY toolbar plumbing
Add openings — GizmoWallAddOpening only fired when a wall was active +
co-selected with a non-host; slabs and roofs got no in-viewport handle.
GizmoHostAddOpening covers all three host types via is_supported_host,
dispatching walls to the axis-projection anchor and slabs/roofs to a
world-Z anchor lifted just above the host's top face (predictable
height regardless of the void's vertical position).

Show openings on hosts with their own parametric-edit toolbar —
GizmoRoofEdition gains an idle-row toggle_openings_gizmo parallel to
the wall's, parked at the cancel-slot X next to the pen. Visible only
when the host carries HasOpenings and the edit triad is idle. Roof
overrides get_element_height to return the mesh's world-AABB top in
object-local Z, so the WHOLE pen-row anchors visibly above sloped or
stepped roof bodies. The wall's idle-row toggle now also hides when
HasOpenings is empty.

Show openings on hosts WITHOUT a parametric-edit toolbar —
GizmoHostToggleOpenings scoped strictly to the fallback case: a single
host selected, HasOpenings non-empty, NOT a path-connectable wall, NOT
a parametric roof. Covers slabs today plus any foreign-authored IfcRoof
without BBIM_Roof. Anchored at object origin XY + world-AABB top Z.
When slab parametric-edit eventually lands, the slab predicate joins
the exclusion list and this gizmo's poll narrows automatically.

Operator move — ToggleWallOpenings was already host-agnostic; renamed
to ToggleHostOpenings in opening.py (bl_idname bim.toggle_host_openings).
Three callers (the wall idle-row binding, GizmoWallFilletToggleOpenings,
and workspace.py's hotkey_A_O for Alt+O) now route through the renamed
operator. The Alt+O binding is surfaced in the operator's
bl_description so it appears in F3 search and hover tooltips.

DRY refactors —
* GizmoWallAddOpening deleted (subsumed by GizmoHostAddOpening)
* tool.Blender.get_object_world_bounding_box added as the world-AABB
  sibling of the existing local helper; 3 inline call sites in
  tool/misc.py (set_object_origin_to_bottom, scale_object_to_height)
  and gizmos.py adopt it (2 other sites in drawing/operator.py and
  project/operator.py inherently need raw transformed corners for
  per-corner plane / NDC tests — not AABB candidates)
* BaseParametricGizmoGroup gains setup_pen_row_toggle_openings_icon +
  update_pen_row_toggle_openings_icon; wall + roof + any future host
  gizmo wire up the idle-row toggle with two one-line calls
* _resolve_active_host shared poll prologue between the two host
  gizmos (gate + selection count + active-in-selected + entity lookup
  + supported-host check)
* HasOpenings non-empty checks at 3 sites route through
  tool.Geometry.has_openings
* hotkey_A_O body collapsed to bpy.ops.bim.toggle_host_openings()

The forward-compat AST guard pinning "must accept fillet-corner walls"
retargets from GizmoWallAddOpening.poll to is_supported_host.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:44 +02:00
Gorgious56 c398deba71 Stack cursor-anchored wall gizmos along screen-up in top view
The extend-X / extend-Z / split icons share the cursor's projected X
on the wall axis, separated only by world Z (floor / cursor / wall
top). World Z collapses to a single screen point in plan view, so
every icon piled onto extend-X's hit target and only the topmost was
clickable.

Two refinements ported from gizmos-8088:

* When ``tool.Blender.is_view_top_down(context)`` reports the camera
  is near plan-view, swap world-Z stacking for screen-up stacking:
  anchor all icons at the floor world position and offset each by
  ``index * CURSOR_STACK_OFFSET`` along ``tool.Blender.get_screen_up_world(context)``.
  Each icon lands in its own screen-space slot regardless of view
  rotation.
* In the same top-down branch, drop ``extend_z_gizmo`` entirely. A
  vertical-intent gizmo has no readable cue when looking down +Z —
  clicking it would mutate the wall in a direction the user can't
  see change.
* Bonus: split's local Z now goes through
  ``core.extrusion_depth_from_vertical_height(props.height, props.x_angle)``
  so the icon lands on the slanted top edge of sloped walls (x_angle
  != 0) instead of the vertical-height target the wall isn't at.

All three helpers (``is_view_top_down``, ``get_screen_up_world``,
``extrusion_depth_from_vertical_height``) already on HEAD from PR2/PR3.
Non-top views unchanged — same world-Z stacking + cascading bumps as
before.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:44 +02:00
Gorgious56 5ce7d6f834 Port WallGizmoPreviewDecorator from gizmos-8088
Hover-gated viewport preview lines that show where a wall-join /
extend / split operator would land before the user clicks. Four
preview paths, each gated on a specific icon's ``is_highlight`` state:

* **Join intersection** — two LAYER2 walls selected in the ``intersect``
  state (non-joined, non-collinear, non-parallel). Draws four lines:
  each wall's axis at both base and top Z, extending from the wall's
  nearer endpoint to the projected XY intersection. The pair of lines
  per wall communicates the full plane the join welds at, not just
  the floor edge.
* **Cursor extend** — single LAYER2 wall, hover on ``extend_x_gizmo``.
  One line from the wall's nearer X endpoint to the cursor's projected
  X on the wall axis.
* **Cursor extend-Z** — hover on ``extend_z_gizmo``. Vertical line at
  the cursor's projected X from wall base to cursor Z (the new total
  height).
* **Cursor split** — hover on ``split_gizmo``. Vertical line at the
  cursor's projected X from wall base to wall top — the cut plane.
  Warning-red colour matches the icon's destructive-action signal.

Hover colour rules for the join preview:

* **Join or Fillet hover** → all four lines highlight in
  ``decorator_color_selected``. Both icons commit a symmetric corner
  meet, so every line is part of the operation.
* **Extend-to-Wall hover** → only the non-active wall's two lines
  (base + top) highlight. The default-direction extend operator
  moves the non-active wall into the active one's axis; only that
  wall's preview should signal motion.
* No hover → all four lines in ``decorations_colour``.

Three coordinated changes:

* ``bim/module/model/wall.py`` gains the ``_classify_wall_join_state``
  wrapper over ``core.classify_wall_join_state`` (feeds the
  ``_are_walls_joined`` flag the core helper expects) AND a
  ``_active_instances`` per-region weakref ClassVar on
  ``GizmoWallJoinIntersection`` populated in ``setup()``. Without the
  weakref registration, the decorator's ``_lookup_active_instance``
  call returns None every frame and the hover gates silently
  evaluate False — the symptom would be preview lines that never
  switch colour. Both pieces ported from gizmos-8088.
* ``bim/module/model/decorator.py`` gains
  ``WallGizmoPreviewDecorator`` (~280 LOC across the four preview
  paths + shared helpers ``_stroke`` /
  ``_active_layer2_wall_for_gizmo_preview`` /
  ``_join_group_hover_state`` / ``_extended_wall_index``). All
  cross-file dependencies (``core.classify_wall_join_state``,
  ``core.wall_join_preview_lines``, ``_stroke_lines_alpha``,
  ``_cursor_icon_hovered``, ``_lookup_active_instance``,
  ``tool.Parametric.is_path_connectable_wall``,
  ``_wall_axis_world_segment_from_geom``) already on HEAD.
* ``bim/handler.py`` wires ``WallGizmoPreviewDecorator.install()`` /
  ``.uninstall()`` alongside the other always-on preview decorators.
  The decorator self-polls every frame; cost is one selection-count
  check + one ``is_highlight`` read when no eligible state is active.

Verified: headless smoke green, ruff + black clean. Live testing
confirms the four preview paths fire correctly when hovering each
icon.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:44 +02:00
Gorgious56 497acc3d19 Stack wall-join trio along screen-up + L/T glyphs
GizmoWallJoinIntersection used to place its icons at state-specific
world points: join at floor Z, extend-to-wall at the active wall's
top Z, fillet stacked screen-up above join. Same XY at different Z
collapses to a single screen pixel in plan / top view, so two icons
became one hit target — invisible from above.

* position_gizmos now always-stacks along screen-up at a wall-top
  anchor in both the joined (unjoin + fillet) and the intersecting
  (extend + join + fillet) states. Order bottom-up is
  extend / L / fillet. Collinear-merge keeps its single boundary
  icon (no stack needed).
* New _stack_anchor_z picks the active wall's top Z (or the taller
  of the two on mid-selection-transition frames). New _stack_at
  lays a tuple of icons along screen-up at the resolved anchor.
* Glyph swap: join_icon -> VIEW3D_GT_wall_corner (L), extend_to_wall_icon
  -> VIEW3D_GT_wall_tee (T). Both classes already existed in
  bim/module/drawing/gizmos.py from an earlier commit; only the
  setup() bl_idname strings changed. The previous arrow-merge /
  arrow-extend pair read as the same direction once stacked.

Forward-compat AST contracts in test_wall_gizmos_forward_compat.py
pin the new invariants: the L and T bl_idnames must appear in
setup(), and position_gizmos must route through _stack_at so a
regression that reintroduces a direct billboarded_at write for any
state-specific icon fails CI before it flattens the stack again.

Also folds in a one-line typo fix in core/spatial.py:
assign_container's per-element can_contain check iterated `e` but
predicate-tested `root_element` (the outer for-loop variable), so
every element in the comprehension was tested against the same
container/element pair. Switch the argument to `e`.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:44 +02:00
Bruno Perdigão ebb3b1ed10 Optimize 2D projection in ray_cast_by_proximity_2d 2026-06-11 18:34:44 +02:00
Bruno Perdigão 1597b30997 Early-terminate solid raycasts in non-xray mode 2026-06-11 18:34:43 +02:00
Bruno Perdigão 777eda3b09 Lazy BVH tree construction in SnapObj 2026-06-11 18:34:43 +02:00
falken10vdl 0ceb61f0c0 Add has_underside_connection method to Model class and update wall regeneration logic 2026-06-11 18:34:43 +02:00
Ryan Schultz f5966f20a8 Fix validate_type corruption; remove debug prints
When validate_type selected a preferred_item from remaining_items
(e.g. the sole IfcBooleanResult in a representation), it left that
item in the list. The subsequent Items filter removed every item,
leaving Items=[] and causing guess_type to return
"MappedRepresentation" — silently corrupting the representation.

Also removes temporary debug print statements added during
investigation of the wall-to-slab extension workflow.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:43 +02:00
Ryan Schultz 5f8688862f Fix duplicate booleans in extend_walls_to_underside
Re-running the operator on the same wall/slab pair created
additional IfcPolygonalFaceSet booleans each time. Now each
wall's existing booleans are removed before re-clipping, and
previously connected slabs are merged with the new selection
so no earlier clips are silently discarded.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:43 +02:00
Ryan Schultz f3e4852f3f Regenerate connected walls when recalculating a slab
When Shift+G is pressed on a LAYER3 element, any LAYER2 walls
connected via IfcRelConnectsElements(TOP) are now re-clipped
to the slab's updated geometry after recalculate_slab runs.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:43 +02:00
Ryan Schultz 8ec946d189 Add extend/regenerate walls to multiple undersides
extend_walls_to_underside now accepts multiple slab/roof
objects in a single operation — all selected non-LAYER2 IFC
elements are treated as clip targets, all LAYER2 elements as
walls. Placement sync is done once upfront; each wall is then
clipped against every selected slab before reloading.

Also adds bim.regenerate_wall_to_underside (Shift+G): after
moving a slab, re-clips connected walls using the existing
IfcRelConnectsElements(TOP) relationship. Old booleans are
removed via remove_representation_item before re-clipping.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:43 +02:00
Ryan Schultz b10c9cd902 Closes #7943: Add regenerate_wall_to_underside operator
When extend_walls_to_underside is applied to a wall and the
roof/slab is later moved, pressing Shift+G now re-clips the
wall to the slab's new position.

The IFC relationship created by connect_wall_to_slab
(IfcRelConnectsElements, Description="TOP") is used to look
up which slabs a wall is clipped to. On regeneration, the
existing manual booleans (IfcPolygonalFaceSet operands) are
cleanly removed via remove_representation_item, then
clip_wall_to_slab is re-applied for each connected slab.

Shift+G on a LAYER2 wall that has a TOP connection now calls
bim.regenerate_wall_to_underside; walls without a connection
continue to call bim.recalculate_wall as before.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:42 +02:00
Ryan Schultz 97cd08ee92 Fix extend_walls_to_underside ridge artifact
When the operator was called twice on the same wall for a
ridge roof, the two IfcPolygonalFaceSet clip solids shared
an exact ridge edge (kissing-solid). OCCT produced spurious
extra vertices at the coincident boundary.

Fix by building the clip solid from a rectangle on the slope
plane that extends slightly past the face edge (1 project
unit margin) rather than the exact face footprint. Adjacent
slope solids now volumetrically overlap at the ridge instead
of sharing a boundary face, which OCCT handles correctly.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:42 +02:00
falken10vdl a002e1e56d Fix assign_container in spatial.py (#8079)
ifc.get_object(element) can return None for IFC elements that aren't loaded as Blender objects (e.g., decomposed sub-elements). 
The loop now skips those instead of passing None into collector.assign().

Cheers!
2026-06-11 18:34:42 +02:00
Tiago Azevedo c559ee0015 Fix sign of temporary offset restore in sweep_along_curve
The temporary-offset workaround (#7408, commit bd57cc8735) subtracts the
directrix centroid (`mean`) from the curve points before building the
sweep near the origin, then must add it back to restore the original
location. The restore negated the sign — `Move(-mean)` instead of
`Move(+mean)` — placing the swept solid at -mean (mirrored through the
origin) rather than its true position.

Only triggers for polyline directrixes (`is_polyhedron()`) whose centroid
is more than 100 m from the origin (`mean.norm() > 1e2`), so models
centered near the origin are unaffected. Models that keep absolute site
coordinates (e.g. many Revit/ODA IFC exports) render affected swept
solids — reinforcing bars, pipes — at a mirrored phantom location far
from the rest of the model.
2026-06-11 18:34:42 +02:00
Gorgious56 d71856d884 Migrate Modifier shim callers + drop the shim block
Completes the PR4/PR5 cleanup the FIXME at tool/blender.py
flagged: every is_<type> / Array.<helper> shim on
tool.Blender.Modifier delegated one-for-one to tool.Parametric /
tool.Array. Callers now reach the canonical home directly, and the
shim block — seven is_<type> classmethods plus the inner class Array
— comes out.

Renames (no semantic change):

* tool.Blender.Modifier.is_<door|railing|roof|stair|wall|window>
  → tool.Parametric.is_<x>
  13 sites across tool/loader.py, bim/import_ifc.py,
  bim/module/geometry/{data,operator}.py, bim/module/model/{door,
  railing,roof,stair,ui,wall,window}.py.

* tool.Blender.Modifier.Array.<helper> → tool.Array.<helper>
  4 sites across tool/root.py, bim/import_ifc.py,
  bim/module/geometry/operator.py.

* test_parametric_registry.py: the two getattr probes that hunt
  predicates by name now look on tool.Parametric. Docstring + the
  test function name (test_every_entry_has_modifier_predicate →
  test_every_entry_has_parametric_predicate) follow the move.

Kept on tool.Blender.Modifier (non-shim, no equivalent on
tool.Parametric): try_applying_edit_mode,
try_canceling_editing_modifier_parameters_or_path,
is_eligible_for_<x>_modifier (×5), is_array_child, is_slab.

Verified: 109 model-lane tests + 8 parametric-registry tests pass
(the one pre-existing failure in test_wall_header_refresh.py is
unrelated — it patches handler.update_bim_tool_props which has been
renamed). git grep for tool\.Blender\.Modifier\.(is_<type>|Array\.)
returns empty. black + ruff clean on every touched file.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:42 +02:00
Gorgious56 0c8b6e93c6 Add GizmoRoofEdition + fix low-slope normals + cancel restore
Ports roof parametric edit gizmo group from gizmos-8088 and folds in
three roof-mesh bug fixes surfaced during live testing.

Port:

* ``CycleRoofGenerationMethod`` operator (bim.cycle_roof_generation_method)
  cycles props.generation_method between "HEIGHT" and "ANGLE". Shift+click
  cycles in reverse via the ``CycleTypeMixin`` contract.
* ``GizmoRoofEdition`` gizmo group: 3 dimension gizmos for height
  (visible in HEIGHT mode) / slope angle with tan/atan2 rise round-trip
  + degree formatter (ANGLE mode) / roof_thickness. All three handles
  anchor at the object's local origin and separate visually via their
  declared axes (height/slope +Z, thickness -Z) — height + slope are
  mutually exclusive via ``visibility_condition`` so they never paint
  at the same time. Anchoring at the origin sidesteps the first-click
  default-identity-matrix symptom that footprint-derived anchoring
  would have hit on a stale ``RoofData`` cache.
* Lifecycle factory swap: explicit ``EnableEditingRoof / CancelEditingRoof
  / FinishEditingRoof`` classes replaced by ``tool.Parametric.build_edit_lifecycle("roof", _RoofEditMixin, ...)``.
  Same bl_idnames out, no external caller changes.
* Registration: ``CycleRoofGenerationMethod`` + ``GizmoRoofEdition``
  added to ``bim/module/model/__init__.py`` classes tuple.
* Tests: ``test_roof_gizmos.py`` covering slope round-trip, visibility
  gates, cycle operator metadata, and origin-anchored positioning.

Bug fixes:

* ``generate_hipped_roof_bmesh`` flipped the bottom slab face's normal
  at low slope angles. The kernel's outward-inference becomes
  ambiguous on near-flat geometry once ``remove_doubles`` and
  internal-face deletion run, and the early ``recalc_face_normals``
  pass at line 389 ran BEFORE the topology was final. A second pass
  on the final closed mesh fixes the eave plane (now reliably points
  down regardless of slope).
* ``bpypolyskel.polygonize`` can emit a face whose vertex list
  contains the same index twice on certain footprint/slope
  combinations (a straight-skeleton ridge collapse). ``bm.faces.new``
  rejects those with ``found the same (BMVert) used multiple times``,
  aborting the whole rebuild. Filter the degenerate faces out so the
  rest of the roof renders.
* ``_RoofEditMixin._restore_viewport_after_cancel`` now rebuilds the
  bmesh from the just-restored draft via ``update_roof_modifier_bmesh``.
  The hook was abstract on ``PathPreservingEditMixin`` and raised
  ``NotImplementedError`` on cancel-after-edit, leaving the user
  stranded.

Also folds in a parallel ``tool/loader.py`` swap from
``tool.Blender.Modifier.is_railing`` to ``tool.Parametric.is_railing``
(consistent with the rest of the loader using ``tool.Parametric.*``).

Verified: headless smoke green, test_parametric_registry.py 8/8,
test_roof_gizmos.py 15/15. ruff + black clean on the touched files.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:42 +02:00
Gorgious56 e1ab5047b0 Fix fillet preview crash + surface openings on fillet walls
Three wall-gizmo fixes:

* GizmoWallFilletPreview crashed on every draw_prepare after the
  DRY-colors refactor moved decoration lookups onto
  self.get_decoration_colors() — that method lives on
  BillboardingGizmoGroupMixin / BaseParametricGizmoGroup, but
  GizmoWallFilletPreview inherited only from bpy.types.GizmoGroup.
  setup() AttributeError'd silently, leaving radius_dim and friends
  unset. Add the mixin to the bases; rename _position_gizmos to
  position_gizmos so the mixin's refresh/draw_prepare dispatch lands
  correctly and drop the now-redundant overrides.

* GizmoWallAddOpening's poll gated on the strict is_wall predicate,
  which rejects fillet-corner walls (no LAYER2 usage by IFC spec).
  Switch to is_path_connectable_wall on both the active and the
  partner-exclusion checks so the add-opening icon surfaces over
  curved corners — matching every other wall-state gizmo's host gate.

* Show / hide openings was only available on LAYER2 walls because
  GizmoWallEdition's parametric edit pipeline (which carries the
  toggle) refuses fillet bodies. Add GizmoWallFilletToggleOpenings,
  a dedicated single-icon group that polls on is_fillet_corner_wall
  and reuses bim.toggle_wall_openings — the body stays untouched.

Forward-compat AST guards in test_wall_gizmos_forward_compat.py pin
both invariants: every wall GizmoGroup that calls
self.get_decoration_colors() must inherit a mixin that provides it,
and GizmoWallAddOpening.poll must keep using the looser predicate.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:42 +02:00
Gorgious56 28491b290a Split update_bim_tool_props commit vs selection
tool.Parametric.refresh_post_commit was calling update_bim_tool_props
after every IFC mutation. The function does two things — refresh
read-only header values (extrusion_depth/length/x_angle) and re-target
user-intent enums (ifc_class, relating_type_id) from the active object.
Doing both on the commit path crashed on IfcAnnotation actives (the
type isn't in the bim_tool ifc_class enum) and silently overwrote the
user's "what to build next" choice on every other element.

Split the function: update_bim_tool_props remains selection-driven and
does both halves; new refresh_bim_tool_headers is header-only and is
what refresh_post_commit now calls. Behaviour on selection change is
preserved. Also ports the upstream PR #8136 try/except guard onto the
props.ifc_class write for the selection-driven path. Adds
test_handler_forward_compat.py to pin both contracts via AST.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:41 +02:00
Gorgious56 645054aa6a Wire array panel buttons to triad lifecycle
Two bugs in BIM_PT_array:

1. The "is this layer in edit mode" predicate compared a BoolProperty
   against an int (props.is_editing == i). Python evaluates False == 0
   as True, so layer 0 always rendered the per-layer edit form even
   when no edit was active — clicking validate/cancel then dispatched
   against a phantom edit state. Switched to
   props.editing_item_index == i, which defaults to -1 and matches
   exactly one layer when an edit is active.

2. The panel's CHECKMARK and CANCEL buttons called bim.edit_array /
   bim.disable_editing_array, a parallel lifecycle that only cleared
   editing_item_index. Entering edit mode via the viewport gizmo
   (bim.enable_editing_array, the triad enter) sets is_editing=True
   and hides array children; the legacy panel exit unwound neither —
   so committing or cancelling from the panel left is_editing=True
   with children hidden, and the viewport gizmo thought the edit was
   still in progress. Re-bound both panel buttons to the canonical
   triad operators (bim.finish_editing_array /
   bim.cancel_editing_array), which _ArrayEditMixin already owns and
   which the viewport gizmo group already uses. Panel and gizmo now
   share one exit path.

The three now-unreachable operators are deleted with their
registration entries: EditArray (bim.edit_array), DisableEditingArray
(bim.disable_editing_array), and EnableEditingArrayItem
(bim.enable_editing_array_item, never called from any UI). The two
test/tool/test_model.py sites that drove bim.edit_array as a commit
step are switched to bim.finish_editing_array.

External scripts or user keymaps bound to bim.edit_array /
bim.disable_editing_array will need to update — the replacements are
bim.finish_editing_array and bim.cancel_editing_array, both taking no
parameters (the layer is read from props.editing_item_index).

Partly generated with the assistance of an AI coding tool.
2026-06-11 18:34:41 +02:00
Gorgious56 95ad96c25e Fix door swing arcs + declarative SwingArcConfig
The recent per-gizmo-prefs cleanup left ``update_swing_gizmos`` with a
stale ``prefs`` reference that raised NameError mid-refresh, so the flip
arc's ``matrix_basis`` was never reassigned and the gizmo drifted to the
world origin. SINGLE_SWING_RIGHT also lacked an X-mirror on the primary
arc, so the swing extended past the door's right edge instead of
sweeping back over the panel.

Five related fixes / additions:

* Drop the leftover ``prefs.decorations_colour[:3]`` per-frame colour
  override (the setup-time ``decorator_color_special`` is the durable
  contract — there's no reason to overwrite it every refresh).
* Add X-mirror to RIGHT-hinged single-panel transforms so the arc
  sweeps back over the door rather than past the right edge.
* Treat DOUBLE_DOOR_SINGLE_SWING as a two-panel layout: 4 arcs total
  (left + right panels, each with its own Y-mirrored flip) scaled to
  ``overall_width / 2``.
* Hide all swing arcs for SLIDING_TO_LEFT / SLIDING_TO_RIGHT /
  DOUBLE_DOOR_SLIDING — sliding doors don't swing. A slide-direction
  indicator is deferred to a separate change.
* Pin ``select_bias = -1000.0`` on every arc gizmo so the big
  quarter-arc hit shapes don't steal clicks from the smaller dimension
  and edit gizmos drawn on top.

Architectural cleanup driven by the same diff: the imperative
4-create + 50-line update block is replaced by a declarative
``swing_arc_props`` list of ``SwingArcConfig`` entries (mirrors the
existing ``dimension_gizmo_props`` pattern). Setup iterates the list
and creates one (main, flip) pair per entry under
``gizmo_swing_arc_<name>`` / ``gizmo_swing_arc_<name>_flip``; update
iterates the same list and positions each pair via the lambdas. Adding
a hypothetical multi-panel variant becomes a config entry rather than
two more attribute names plus a transform branch.

``ToggleDoorSwing`` gets a ``description`` classmethod that returns
user-facing wording per ``flip_geometry`` branch so the tooltip on
hover stops reading like operator internals.

``test/bim/module/model/test_door_gizmos.py`` (new) pins the
per-door-type contract: 11 cases covering LEFT / RIGHT hinge positions,
DOUBLE_SWING parity with SINGLE_SWING, DOUBLE_DOOR 4-arc layout, the
sliding-types hide invariant, ``is_editing=False`` hide invariant,
flip-arc matrix re-assignment, and world-matrix pre-multiplication.

Verified: ``pytest test/bim/module/model/test_door_gizmos.py`` 11/11
green; combined wall + stair + door gizmo lanes 37/37 green; ruff +
black clean on the three touched files.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:41 +02:00
Gorgious56 a2d600b9af Add IconSlot placeholders + stair xN tread label
Add a clickable "xN" badge to GizmoStairEdition's edit row, mirroring
the array's popup-input UX: click opens a number dialog (no more
shift+click-into-modal). Text-only — no 2x2 grid glyph.

Structural changes that enable this cleanly:

* IconSlot.placeholder=True: slots reserve an X position in the row
  without auto-creating a gizmo. Subclasses resolve the reserved X via
  _slot_x_positions()[name] to place their own dynamic gizmos. Drops
  the brittle "remember to add extra_gap_before" workaround that would
  silently rot on slot reorders.

* Array bug fix: the count badge collided with the "-" icon because
  the slot manager placed count_minus at the cycle position (X=0.87)
  where ICON_NUMBER_X also lives. Migrating the badge to a placeholder
  slot lets the manager allocate the X naturally and the "-" no longer
  overlaps. ICON_NUMBER_X constant removed.

* IntegerInputDialogMixin in parametric_lifecycle.py: extracts the
  popup-dialog plumbing shared between InputArrayCount and the new
  InputStairTreads. Subclasses declare an IntProperty + attr_name +
  props_getter; the mixin owns invoke/execute. _resolve_props helper
  factors the common obj/props/requires_editing prologue.

Tests: BIM_GT_count_label registration; IconSlot placeholder contract
(no gizmo_idname required; gizmo_attrs() returns empty); the stair
edit-row slot layout reserves the label position between tread_lock
and plus at one ICON_ARRAY_GAP each; visibility propagates from
props.is_editing.

Partly generated with the assistance of an AI coding tool.
2026-06-11 18:34:41 +02:00
Gorgious56 fbfbe93550 Drop per-gizmo preferences + fix dynamic-wall face normals + DRY colors
Three related cleanups in one pass:

* **Per-gizmo preferences removed.** The ``visibility_pref`` field on
  IconSlot, the ``prefs.gizmos.<feature>.<icon>`` PropertyGroups, and
  the dispatcher that surfaced them in the addon preferences UI are
  all gone. ``update_gizmo_visibility`` loses its ``pref_enabled``
  parameter — visibility is now driven purely by editing state and
  modal gating. bim/ui.py drops ~257 lines of dead PropertyGroup
  definitions; bim/__init__.py and tool/parametric.py shed their
  matching wiring; door / wall slot declarations stop referencing
  the now-nonexistent prefs.

* **Dynamic-wall face normals fixed.** ``regenerate_wall_mesh_from_props``
  in wall.py now calls ``bmesh.ops.recalc_face_normals`` before writing
  the mesh. Without it, walls regenerated from the parametric edit
  draft could ship with inward-facing normals on some faces, which
  rendered as visual holes under any backface-cull or normal-aware
  shading. ``test/bim/module/model/test_wall_preview_mesh.py`` pins
  the invariant (every face's normal points away from the wall centre).

* **Color constants DRY.** ``COLOR_RED`` / ``COLOR_GREEN`` /
  ``COLOR_BLUE`` / ``COLOR_NEUTRAL`` now live at module scope in
  gizmos.py; the BaseParametricGizmoGroup class attributes alias the
  same tuples so ``self.COLOR_GREEN`` keeps working. IconSlot
  declarations in stair.py (plus / minus) and array.py (count_minus /
  count_plus / delete) now reference the named constants instead of
  duplicating the RGB tuples inline.

Verified: headless smoke green at 1267 BIM_OT_ classes,
test_parametric_registry.py 8/8, wall lane 31/31 (includes the new
preview-mesh test). ruff + black clean on the touched files.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:41 +02:00
Gorgious56 782f25bd31 Highlight partner wall on link-toggle hover
Hovering a wall-junction link-toggle icon today only swaps the icon
shape — the user doesn't see which wall the click will disconnect from
until after they click. ATPATH (T-junction) configurations especially
make the partner ambiguous when multiple connections sit close together.

On hover, paint a wireframe bbox around the partner wall using the same
shader, constants and color the array module already established for
its layer-children highlight (POLYLINE_UNIFORM_COLOR, decorator_color_special,
line width 1.8, alpha 0.8). The line-width / alpha constants in decorator.py
are renamed from _ARRAY_LAYER_BBOX_LINE_* to _BBOX_HIGHLIGHT_LINE_* and
shared between draw_array_layer_children_bbox and the new
draw_wall_partner_bbox so the two highlights stay in lockstep.

The trigger lives in a new GizmoWallLinkToggle subclass in wall.py
which keeps the base gizmos.GizmoLinkToggle generic (per the
generic-naming convention for shared widgets). The subclass's draw()
calls super().draw(context) then on self.is_highlight outlines its
partner_obj via the shared decorator helper. Same trigger pattern as
GizmoArrayLayerIndicator.

Blender's Gizmo API exposes target_set_operator but no symmetric
getter, so the partner reference can't be read back from the bound
operator handle. Instead GizmoWallUnjoinSingle.position_gizmos
mirrors the resolved partner_obj onto each visible icon every frame
next to the existing other_wall_guid write — the icon's draw() reads
from its own __slots__-declared attribute.

A forward-compat AST test pins the contract: GizmoWallLinkToggle.draw
must reference is_highlight and call draw_wall_partner_bbox. Catches
the regression where someone tidies the draw() override into super()
or replaces the shared helper with an ad-hoc draw call.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:41 +02:00
Gorgious56 f2868c2631 Replace hardcoded icon-X constants with IconSlot layout manager
The parametric edit toolbar row used to assign each feature icon its
own ICON_<NAME>_X constant, with a separate FEATURE_ICON_MAX_X override
each subclass had to bump whenever a new icon was added. Forgetting the
bump silently collided icons — wall's rotate icon and the array button
both landed at X=1.24 in edit mode.

The new IconSlot dataclass + feature_slots tuple replace the
constants-and-override pattern with order-driven positioning: the
layout manager assigns each slot an X from its tuple index plus a
uniform ICON_ARRAY_GAP. Adding an icon is now a one-line append; the
"forget to bump" failure mode is structurally impossible.

Slot capabilities cover every existing icon-row shape:
* Single icon (wall rotate, array delete).
* N-variant slots — N gizmos at the same X with one visible per frame
  via a subclass picker (stair tread-lock open/closed, wall baseline
  exterior/center/interior). Pair becomes the N=2 case; triplet the
  N=3 case. Variant idnames can be authored either as a tuple of
  explicit names or as a string prefix that auto-suffixes _<variant>.
* Visibility prefs gate slot rendering without reflowing the row —
  hidden slots still consume their X position.
* Extra per-slot gap before for visual separation (array's delete
  trails the routine controls by an extra 0.2 m).
* Operator props forwarded to target_set_operator so adjusters
  (+/-, increment) and generic toggles (property_name=...) work.

When the cycle slot is unused, feature slots collapse into the cycle
position so the row stays tight — that's how wall's baseline triplet
sits at X=0.87 without a gap before it.

Three subclasses migrate to the new system:
* wall.py — rotate icon + baseline triplet variants. Drops
  ICON_ROTATE_X, _BASELINE_GIZMO_ATTRS, the manual triplet creation
  loop, and the matching positioning block in _update_icon_row_extras
  (it now just picks variant visibility).
* stair.py — tread_lock pair (open/closed) + plus + minus.
  _update_editing_icon_positions reads slot X via _slot_x_positions
  instead of three hardcoded constants. Also fixes the standalone
  total_length_lock gizmo, which was broken since PR4 split
  VIEW3D_GT_lock into open/closed pair (caller wasn't updated).
* array.py — count_minus + count_plus + method + delete (with
  extra_gap_before=0.20 to separate the destructive action).
  Drops the manual edit-row positioning loop entirely; the base
  loop handles it. GizmoArrayChild now inherits BillboardingGizmoGroupMixin
  and uses the shared setup_icon_gizmo helper, dropping its
  duplicated _make_icon wrapper.

Two helpers added on BillboardingGizmoGroupMixin to fold the duplicated
prefs/color preamble that appeared at the top of six wall gizmo setups
plus the array-child setup:
* get_decoration_colors() — (decorations_colour, decorator_color_selected),
  the active-state pair.
* get_unselected_decoration_colors() — (decorator_color_unselected,
  decorator_color_selected) for gizmos surfaced on already-selected
  geometry that should not pull focus.

Verified: headless smoke green at 1267 BIM_OT_ classes,
test_parametric_registry.py 8/8 pass, wall lane 29/29 pass,
model lane unchanged at 135 pass + 7 pre-existing v0.8.0 failures
(no regressions). ruff + black clean.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:41 +02:00
Gorgious56 a48f326ab2 Add link-toggle hover gizmo for wall junctions
The previous single-wall unjoin gizmo used a bracket-pair icon
(VIEW3D_GT_unjoin) that reads as "unjoin" only after you know what
it is, with no clear "linked" inverse — closing the brackets to
suggest the connected state collapses to a hollow square that
doesn't read as a link at all.

Add GizmoLinkToggle (VIEW3D_GT_link_toggle): two filled dots joined
by a horizontal connector in the default state. On hover the two
halves shear vertically apart — left dot+stub slip down as a unit,
right dot+stub slip up — with a horizontal gap at the centre,
signalling that a click will sever the underlying connection. The
glyph lives next to the generic icon classes (GizmoLockOpen/Closed,
GizmoArc) so any path / link / pair-of-connected-items context can
reuse it; it isn't wall-specific despite the first caller.

The class keeps its own per-state GPUBatch cache so the shape swap
on hover doesn't allocate per frame. The hit-shape is sourced from
the broken form (the larger bbox of the two states) so the cursor
doesn't lose hover at the offset dots' outer edges and flicker
between states.

GizmoWallUnjoinSingle.setup() now requests VIEW3D_GT_link_toggle.
The operator binding (bim.unjoin_wall_path_connection), the
POOL_SIZE, and the per-frame partner-GUID write are unchanged.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:41 +02:00
Gorgious56 49fe0756fa Fix spurious X/Y rotation on fillet corner wall
When the two source walls were placed at different elevations, the
fillet corner wall ended up with sub-degree X and Y Euler rotations
even though both source walls had only a Z rotation.

Cause: _apply_fillet_corner_geometry derived the corner's local X
axis from `chord = tangent_b - tangent_a` (a 3D vector). With walls
at different Z, `chord.z` was non-zero, so `x_dir = chord.normalized()`
inherited that Z component. The Z axis was already hardcoded to world
Z, so x_dir and z_dir were no longer orthogonal — the resulting
matrix_world was non-orthonormal, and Blender's Euler decomposition
surfaced the skew as the visible X/Y rotation drift.

Project the chord to the XY plane before normalising so x_dir is
strictly XY-aligned and orthogonal to z_dir. The corner wall is now
placed at wall A's elevation with a pure Z rotation, which matches
the user's expectation when both inputs are Z-aligned regardless of
their relative elevation.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:40 +02:00
Gorgious56 1534003e51 Fix fillet partner missing from wall unjoin gizmo
GizmoWallUnjoinSingle.poll accepts fillet-corner walls via the looser
tool.Parametric.is_path_connectable_wall predicate (fillet corners
have no LAYER2 usage by IFC spec, but they still participate in
IfcRelConnectsPathElements). The partner filter inside
_iter_path_connections used the stricter tool.Blender.Modifier.is_wall
(LAYER2-only), so adjacent LAYER2 walls silently dropped their
fillet-corner partners from the connection list — the unjoin icon
appeared when the fillet wall itself was selected but not on either
of its LAYER2 neighbours.

Switch the partner filter to is_path_connectable_wall so host and
partner predicates match. Add a regression test for the fillet case
and an AST forward-compat guard pinning the predicate symbol so a
future "tidy the imports" can't silently re-introduce the asymmetry.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:40 +02:00
Gorgious56 ea487fb17c Add array parametric edit lifecycle + GizmoArrayEdition / Child
Ports the array parametric-edit lifecycle, gizmo group, child guard,
per-layer ARRAY entry icons, and the array bbox decorators
(preview + selection highlight + layer-children) from gizmos-8088.
Restores the array_gizmo icon's positioning + visibility in the
framework's parametric edit row.

Registry (tool/parametric.py):
* EDIT_TYPES adds ParametricObject("array", supports_build_edit_lifecycle=True).
  _ArrayEditMixin in array.py feeds build_edit_lifecycle which auto-
  generates EnableEditingArray / FinishEditingArray / CancelEditingArray
  with the conventional bl_idnames the gizmo references.

tool/blender.py:
* Adds is_array predicate wrapper around tool.Parametric.is_array.
  The registry contract test test_every_entry_has_modifier_predicate
  enforces every EDIT_TYPES entry has a matching is_<name> wrapper on
  tool.Blender.Modifier.

array.py (+1130 LOC port from gizmos-8088):
* _ArrayEditMixin(ParametricEditMixinBase) drives the auto-generated
  enable / finish / cancel lifecycle.
* GizmoArrayEdition: validate + cancel + count display + +/- adjusters
  + method toggle + delete button + per-layer ARRAY entry icons
  (preallocated pool of MAX_LAYER_GIZMOS=8).
* GizmoArrayChild: child-array gizmo for the array-replica case.
* EditArrayFromChild: resolves the spawning layer via
  tool.Array.get_child_layer_index so clicking a child's array gizmo
  opens the layer that produced that child rather than always layer 0
  (the gizmos-8088 source itself hardcoded item=0; HEAD has the helper
  to do it right).
* New operators: EnableEditingArrayItem, ArrayParentGizmoClick,
  ArrayGizmoClick, ToggleArrayMethod, RemoveArrayLayerFromEdit,
  InputArrayCount, AdjustArrayCount.

prop.py: BIMArrayProperties gets per_child_opening BoolProperty
(when the array parent fills a host, give each child its own
opening + filling pair).

Bug fix: guard update_relating_array_from_object against the
cleanup-time None set. _finish_one writes relating_array_object = None
to clear the source-array reference; that fired the update callback,
which dispatched bpy.ops.bim.enable_editing_array(item=self.is_editing).
With is_editing just flipped to False, the bool coerced to 0 and
re-opened layer-0 edit immediately after every validate. The guard
short-circuits on None; item is also fixed to 0 (the bool-as-layer-
index was always meaningless for the legitimate user-pick path).

decorator.py (+312 LOC, all ports from gizmos-8088):
* bbox_world_edges / draw_polyline_segments / _BBOX_EDGES - shared
  geometry helpers usable across array decorators.
* draw_array_layer_children_bbox - green wireframe bbox per child of
  one array layer, drawn inline from a gizmo's draw() so the highlight
  tracks the hover cursor without POST_VIEW lag.
* ArrayPreviewDecorator - faint cyan ghost bboxes at each future
  array instance during the edit lifecycle (offset math mirrors
  Model.regenerate_array, gated on props.is_editing).
* ArraySelectionHighlightDecorator - bounding-box overlay surfacing
  the array family of the selected object. Child selected -> parent
  in special color + siblings in unselected color; parent selected
  (idle) -> all children in unselected color. TokenCache-backed.

handler.py: imports + uninstall/install the 2 always-on decorators in
_install_viewport_overlays. Both self-poll, so installation has no
cost when no array is selected / in edit mode.

Registration (bim/module/model/__init__.py):
* Adds the 3 lifecycle classes generated by build_edit_lifecycle
  (CancelEditingArray, EnableEditingArray, FinishEditingArray) -
  they exist as module-level names but are only visible to Blender's
  operator registry when included in the classes tuple.
* Adds the 8 new operators + 2 new gizmo groups in alphabetical order.

gizmos.py: restores the array_gizmo icon position + visibility block
in BaseParametricGizmoGroup.update_editing_gizmos. Was force-hidden
in c250b2c1a because no array gizmo existed; the icon's plumbing
comes back online now that GizmoArrayEdition is registered.

Verified by test/bim/test_parametric_registry.py: all 8 tests pass -
enable/finish/cancel ops resolve, PropertyGroup attached, is_array
predicate present, predicate is total on non-matching elements.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:40 +02:00
Gorgious56 e0ad34ffd4 Drop duplicate _path_connection_location_world in wall.py
PR3 shipped tool.Wall.path_connection_location_world; the local
_path_connection_location_world added in PR4 commit 70845e4dd
duplicated the same logic. The only caller in wall.py already uses
the tool method (line 3687 area), so the local helper has been
dead code since the migration in 7e5e7b8d6 routed _get_wall_geom_cached
to tool.Wall.read_geometry. Drop it.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:40 +02:00
Gorgious56 12f9e377c8 Route _has_material_styles through tool.Root.has_material_styles
Pre-existing architectural smell on v0.8.0: core/root.py.copy_class
called a module-level _has_material_styles helper that did
ifcopenshell.util.element.get_materials() directly, bypassing the
Prophecy mock seam that every other branch in copy_class flowed
through. Symptom: test/core/test_root.py::TestCopyClass::
test_AAAAAAAAAAAA passed mock strings into copy_class, the helper
called .is_a() on the string, AttributeError.

Move the check to tool.Root.has_material_styles (paired with
assign_body_styles — they're called in sequence as "is there a
material style? if not, assign body style"). core/root.py now
calls root.has_material_styles(new) like every other dependency,
fixing the test failure and dropping the ifcopenshell.util.element
import that was the only consumer of the ifcopenshell import at
module load in core/root.py.

* core/tool.py: add abstract has_material_styles to Root interface.
* tool/root.py: add concrete classmethod near assign_body_styles.
* core/root.py: replace _has_material_styles helper call site with
  root.has_material_styles; drop the local helper and its import.
* test/core/test_root.py: add the new mock expectation
  root.has_material_styles("element").will_return(False) before the
  existing assign_body_styles expectation.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:40 +02:00
Gorgious56 669e5c2aed Add behaviour-contract tests for PR4 surfaces
Three test files covering PR4's new surfaces — preview registry,
wall-gizmo poll behaviour, fillet operator registration. Every test
walks the live registry or class hierarchy instead of hard-coding
preview keys, operator names, or helper function names, so adding a
new preview / wall gizmo group / fillet operator exercises the same
invariants without test edits.

test_preview_base.py (6 tests):
* RegistryContract: every PREVIEW_CANCEL_OPS entry resolves to a
  callable cancel operator on bpy.ops.bim.
* GetPreviewPropsTolerance: get_preview_props returns None for
  contexts without a scene (regression guard for the SimpleNamespace
  bug fixed in commit ee63137c6).
* ActivationCycle (registry-driven loop): any_preview_active toggles
  with each registered preview's is_active flag;
  discard_pending_previews clears every active flag across every
  registered preview.
* SaveOnDiscardWired: locates the bim.save_project operator
  dynamically and verifies its execute path references the discard
  helper by its actual __name__.

test_wall_gizmo_poll_gate.py (4 tests):
* WallGizmoGroupsHideDuringPreview: walks the wall module for
  bpy.types.GizmoGroup subclasses (skips preview-owner exceptions
  whose bl_idname contains 'preview'), mocks any_preview_active to
  True, and asserts every discovered gizmo's poll returns False.
* BaseParametricGizmoPollHidesDuringPreview: mirrors the test for
  the cross-feature parametric framework base class.

test_fillet_operators.py (3 tests):
* FilletOperatorsRegistered: at-least-four-ops + every-discovered-op-
  is-callable. Catches accidental deregistration.
* EnableRejectsIneligibleSelection: poll returns False without a
  selection so the operator is greyed-out in menus.

State-clearing tests via bpy.ops.bim.cancel_wall_fillet_preview() are
deliberately omitted — the operator early-returns when context.screen
is unattached and prior tests in the model lane can leave the screen
in that state, making the dispatch path inherently flaky. Live testing
covers the behaviour.

Net: 13 tests pass cleanly in both single-file and full model lane.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:40 +02:00
Gorgious56 3f5273744d Discard previews on IFC save + harden preview-active gate
Save-path:
* SaveProject._execute (project/operator.py) now calls
  preview_base.discard_pending_previews(context.scene) right after
  tool.Parametric.commit_pending_edits(). Previews are session-
  transient — discard rather than commit. Sibling gizmo polls gate
  on each preview's is_active flag; a stuck flag persisted through
  the save would silently hide them on reload. Mirrors the pattern
  already in gizmos-8088.

Preview-active gate hardening:
* preview_base.get_preview_props tolerates contexts without a
  ``scene`` attribute. Pre-existing tests use SimpleNamespace mocks
  for the context; the previous getattr(context.scene, ...) raised
  AttributeError before the inner default kicked in.

Test update:
* test_wall_header_refresh.test_geom_generation_invalidates_wall_geom_cache
  patches tool.Wall.read_geometry instead of the now-deleted local
  wall._read_wall_geometry (commit 7e5e7b8d6 migrated the call site).

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:40 +02:00
Gorgious56 d6f55b0bf0 Drop wall.py local read_geometry + validate dupes + relax gates
Two cohesive cleanups in one commit.

A. Migrate wall.py to PR3-absorbed tool methods (fixes bug 4: pen icon
missing on fillet corner walls):

PR3 shipped tool.Wall.read_geometry + tool.Wall.validate_for_parametric_edit
but wall.py kept local duplicates predating that work. The local
_read_wall_geometry guards on tool.Blender.Modifier.is_wall (LAYER2-only)
while the tool method guards on tool.Parametric.is_path_connectable_wall
(LAYER2 OR fillet corner). Consequence: _get_wall_geom_cached → local
_read_wall_geometry returned None for every fillet corner →
GizmoWallFilletReedit.position_gizmos hit `if geom is None: hide` →
pen icon was unreachable for every fillet corner the user created.

Three _read_wall_geometry callers migrated to tool.Wall.read_geometry
(_read_wall_state_into_props, _get_wall_geom_cached,
GizmoWallJoinIntersection.position_gizmos). Two
_validate_wall_for_parametric_edit callers migrated to
tool.Wall.validate_for_parametric_edit (_maybe_resync_wall_props_from_ifc,
EnableEditingWall._execute). Local helpers deleted; docstring references
updated.

B. Drop over-restrictive gizmo gates (fixes bug 1: join icons missing
when walls intersect away from endpoints):

GizmoWallJoinIntersection.position_gizmos no longer hides itself when
the projected intersection lands further than MAX_DISTANCE_TO_ENDPOINT_
FACTOR (0.75 wall lengths) from any endpoint. The remaining
PARALLEL_DOT_THRESHOLD (cos 2°) gate via project_axis_intersection
returns None for near-parallel walls and is the only correctness bound;
distance from endpoints is a UI concern, not a geometric one.

GizmoWallFilletReedit.poll drops the has_a / has_b ConnectedFrom +
ConnectedTo guard — the IsFilletCorner pset is the authoritative signal.
EnableWallFilletPreviewFromCorner.execute already separately validates
both neighbour connections and reports a user-facing error if either
side is disconnected.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:40 +02:00
Gorgious56 bf6fd52786 Hide sister gizmos during preview + ESC cancels + DRY wall polls
Three live-session regressions surfaced after the fillet feature
landed.

Sister gizmos competed with the active preview:
* preview_base.any_preview_active(context): new helper iterates the
  PREVIEW_CANCEL_OPS registry and returns True if any preview is open.
  Future previews registered there automatically gate sister gizmos.
* BaseParametricGizmoGroup.poll (gizmos.py): short-circuits on
  any_preview_active so every parametric gizmo (door/window/stair/
  roof/railing/wall edition) hides during ANY preview.
* The 4 wall gizmo groups with explicit polls (GizmoWallAddOpening,
  GizmoWallExtendVertically, GizmoWallJoinIntersection,
  GizmoWallUnjoinSingle) + GizmoWallFilletReedit gain the same gate.

DRY: extract _wall_gizmo_poll_gate(context):
* 5 wall gizmo polls each duplicated the 2 pre-flight checks
  (viewport-gizmos enabled + no preview active). The helper centralises
  them — each poll becomes a single short-circuit line followed by its
  per-feature selection inspection.

ESC cancels the active preview:
* try_cancel_active_preview already existed in preview_base since PR3
  but had no caller. Hooked into OverrideEscape.execute (geometry/
  operator.py) as a new elif branch — same keymap that already cancels
  pen gizmo edit mode + item mode + edit mode + aggregate mode. Order
  in the branch chain matters: try preview cancel before falling back
  to try_canceling_editing_modifier_parameters_or_path so the in-
  flight preview wins over a stale modifier-edit cancel attempt.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:39 +02:00
Gorgious56 9f748fa4a9 Add wall-fillet feature: operators, gizmos, decorator
End-to-end fillet flow on top of the helpers + recreate_wall hook
(landed in the previous commit). Users select two LAYER2 walls, click
the fillet entry icon, drag the live radius widget, and validate to
replace the corner with a curved LAYER2 corner wall (banana body).

Operators (5):
* EnableWallFilletPreview: 2-wall selection → validates LAYER2 +
  straight axis + zero-slope + intersect-or-joined state → seeds the
  preview props with a default radius computed from the shorter
  available leg.
* FinishWallFilletPreview: dispatches CreateWallFillet with the tuned
  radius; clears preview state on FINISHED, preserves it on failure so
  the user can re-tune without re-selecting.
* CancelWallFilletPreview: clears preview state, no IFC mutation.
* EnableWallFilletPreviewFromCorner: pen-icon re-edit on an existing
  fillet corner — pre-fills the preview from the corner's BBIM_Wall
  pset + walks the inverse graph to recover wall A and wall B.
* CreateWallFillet: deletes any prior corner + A↔B path connection,
  shortens A and B to the tangent points, instantiates a corner wall
  from A's type, unassigns the swept-layer material/type (the explicit
  banana body MUST own its geometry), assigns the dominant material,
  rebuilds the body, sets a straight 2-point chord axis, stores
  BBIM_Wall.IsFilletCorner+FilletRadius, reconnects A and B to the
  corner with NOTDEFINED on the corner's side.

Gizmo groups (2 new + entry icon on existing):
* GizmoWallFilletPreview: visible while a preview is active. Bundles
  a radius_dim widget at the arc apex, a trim_dim widget along wall A
  expressing the same DOF via the leg setback distance
  (trim = |radius| * tan(sweep/2)), and validate / cancel icons
  anchored above the apex in screen-up.
* GizmoWallFilletReedit: pen-icon entry on an existing fillet corner
  wall (single-selection, BBIM_Wall.IsFilletCorner set, both neighbour
  connections present). Mutually exclusive with an active preview.
* GizmoWallJoinIntersection now stacks a fillet entry icon
  (VIEW3D_GT_fillet → bim.enable_wall_fillet_preview) above the
  existing join/unjoin icon in the joined and intersect state branches.

Property + decorator infrastructure:
* prop.py: BIMWallFilletPreviewProperties (Scene-level draft) +
  BIMPreviewProperties umbrella with only the wall_fillet pointer.
  The umbrella is the seam preview_base.py (landed in PR3) already
  reads via getattr(scene, "BIMPreviewProperties", None).
* decorator.py: _stroke_lines_alpha helper + WallFilletPreviewDecorator.
  Polls is_active; renders leg projections + arc + arc-center
  construction lines from tool.Wall.compute_wall_fillet_geometry.
* __init__.py: registers operators + gizmo groups + property groups +
  wires Scene.BIMPreviewProperties.
* handler.py: WallFilletPreviewDecorator.install/uninstall in
  _install_decorators — always installed, self-polls on is_active.

Drive-by: extract gizmo.get_screen_up(billboard_rot) helper —
the local +Y of a billboard rotation is the camera's screen-up world
direction. Replaces 4 inline `billboard_rot @ Vector((0.0, 1.0, 0.0))`
sites added across the fillet feature's gizmo groups.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:39 +02:00
Gorgious56 eeede522d9 Add wall-fillet helper functions + recreate_wall hook
Eleven module-level helpers in wall.py that the upcoming wall-fillet
operators + gizmo groups depend on. Each is self-contained or
references only helpers earlier in the file; the operators and
gizmos themselves land in follow-up commits.

* _wall_fillet_props / _wall_fillet_preview_active /
  _wall_fillet_preview_walls: thin read-side accessors over the
  BIMPreviewProperties.wall_fillet pointer (added with the
  operators commit). Safe today: get_preview_props returns None
  until the pointer is attached.
* _walls_have_zero_slope_for_fillet: validates that input walls
  are vertical (x_angle ~ 0); slanted-extrusion fillets require
  swept-along-curve geometry the banana profile builder doesn't
  support.
* _build_curved_corner_body_representation: builds the banana
  (annular sector) IfcExtrudedAreaSolid as a polyline-tessellated
  IfcIndexedPolyCurve.
* _apply_fillet_corner_geometry: positions the corner wall at
  tangent_a and rebuilds its body. Shared by the creation operator
  and the regenerate path.
* _resolve_two_walls: pulls (active, other) from a 2-wall
  selection, validates both as LAYER2 + straight-axis + not-already-
  a-fillet-corner.
* _pick_dominant_wall_material: returns the thickest layer's
  material from an element's IfcMaterialLayerSet / Usage.
* regenerate_fillet_corner_wall: re-runs the geometry build from
  BBIM_Wall.FilletRadius + current neighbour layer parameters.
  Called by tool.Model.recreate_wall when the IsFilletCorner pset
  is set; the FIXME(PR4) placeholder in recreate_wall is dropped.
* _wall_fillet_gizmo_x_matrix: 4x4 placement matrix with local +X
  aligned to a world-space direction; used by the fillet preview
  gizmo group.

Centralises the IsFilletCorner pset read as
tool.Parametric.is_fillet_corner_wall — replaces 3 inline
get_pset(element, "BBIM_Wall", "IsFilletCorner") sites
(tool.Model.recreate_wall, tool.Model.recalculate_walls,
tool.Parametric.is_path_connectable_wall) plus the new
_resolve_two_walls call.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:39 +02:00
Gorgious56 d955763f63 Gate parametric-edit array gizmo until integration completes
The framework's parametric-edit icon row currently binds an array
icon to bim.add_array_from_feature_edit, but the supporting per-
feature add-array flow and gizmo positioning haven't fully landed.
Showing the icon today lets the user click it and trigger a half-
wired flow.

Force the icon hidden inside the props.is_editing branch of
BaseParametricGizmoGroup.update_editing_gizmos. The else-branch
(not editing) already hides it, so this just mirrors that behavior
during edit mode. Drop this gate when array integration completes
to re-enable the icon position + visibility plumbing.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:39 +02:00
Gorgious56 93c51c1a39 Add cursor-aware extend-arrow flip on wall edit gizmos
The extend-X / extend-Z icons in GizmoWallEdition's cursor row are
billboarded toward the camera; without orientation polish they
always point in the same screen-space direction regardless of which
wall endpoint the click will move (or whether the cursor sits above
or below the wall top). New helper mirrors the icon's local-X (extend-X)
or local-Y (extend-Z) axis so each arrow points toward the end it
will move:

* Extend-X: walk wall midpoint to figure out which endpoint stays
  fixed (cursor past midpoint → ATSTART stays; cursor before midpoint
  → ATEND stays). Project the fixed endpoint into screen-space and
  flip the arrow when the gizmo's anchor sits on the same side.
* Extend-Z: flip when the cursor is below the wall top (within
  EXTEND_FLIP_EPSILON tolerance).

Called once per resolved cursor gizmo from
``GizmoWallEdition._update_cursor_gizmos``, after the gizmo's
``matrix_basis`` is set by ``gizmo.billboarded_at``. Reuses
``gizmo.should_flip_extend_arrow`` + ``EXTEND_FLIP_MIRROR_X/Y`` +
``EXTEND_FLIP_EPSILON`` already on tool.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:39 +02:00
Gorgious56 deaf090a50 Add single-wall unjoin operator + gizmo group
GizmoWallJoinIntersection's unjoin only fires when exactly two walls
are selected and surfaces one icon at their shared corner — useless
when the wall has 3+ joins and the user wants to disconnect just one.

* UnjoinWallPathConnection: surgical counterpart to UnjoinWalls.
  Disconnects the active wall from a single partner wall identified
  by IFC GlobalId (invariant under Blender-object renames + file
  save/reload + undo). Walks both inverse arrays of the active wall
  for the specific IfcRelConnectsPathElements joining the pair —
  matches DumbWallJoiner.split's pattern and avoids disconnect_path's
  direction-sensitivity. Resyncs both walls' draft props after the
  recreate_wall pass.
* GizmoWallUnjoinSingle: activates on exactly-one selected
  LAYER2 wall. Preallocates a pool of 16 unjoin icons (Blender forbids
  gizmo allocation outside setup(); ATSTART + ATEND + ATPATH rels are
  rarely more than a handful). Per-frame, iterates _iter_path_connections,
  positions one billboarded icon at each join via
  tool.Wall.path_connection_location_world, and hides the rest. Each
  visible icon's bound operator carries the partner GlobalId, so a
  click removes only that one rel.
* model/__init__.py: register both classes alphabetically.

Mutually exclusive with GizmoWallJoinIntersection via poll() — that
group requires len(selected) == 2; this one requires 1.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:39 +02:00
Gorgious56 db94877d8b Add wall path-connection inverse-walk helpers
The single-wall unjoin gizmo needs to enumerate every
IfcRelConnectsPathElements a wall participates in, regardless of which
side of the rel the wall was authored on, and place an icon at each
join's physical location. Two helpers carry that work:

_path_connection_location_world wraps core.compute_path_connection_location
at the Vector boundary. _iter_path_connections walks ConnectedTo +
ConnectedFrom, normalises orientation to (other, self_ct, other_ct),
and filters non-wall partners + None refs so per-frame gizmo positioning
survives malformed IFC.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:39 +02:00
Gorgious56 8e93fde930 Add wall draft-resync helper + wire 6 mutation operators
After a one-shot wall IFC mutation (unjoin / split / merge / extend /
join-at-corner …) the always-visible gizmos on the OTHER side of the
join can be left reading stale ``BIMWallProperties`` — the IFC
geometry moved but the draft props that drive the gizmo handles still
point at the pre-mutation numbers, so a subsequent edit-mode enter
shows the wall at its old length / position.

* New ``_maybe_resync_wall_props_from_ifc(obj)``: re-primes a single
  wall's draft props from current IFC, with guards for non-walls,
  non-parametric walls, and walls in an active draft session (the
  draft is then the source of truth, not IFC). Must run from an
  operator ``_execute`` — ID writes from gizmo refresh raise.
* New ``_resync_walls_after_mutation(objs)``: iterates the above
  across a selection.
* Six existing mutation operators gain a resync call after their
  ``core.*`` / ``DumbWallJoiner`` mutation completes:
  UnjoinWalls, ExtendWallsToUnderside, ExtendWallsToWall, SplitWall,
  MergeWall, JoinWallsIntersection. MergeWall resyncs only the
  surviving wall — the active wall is the deletion target.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:39 +02:00
Gorgious56 f28b901a49 Fix parametric framework live-session regressions
Bundle of bugs surfaced when exercising the new gizmo framework
end-to-end in a live Blender session after the
bim/module/drawing/gizmos.py refactor + TypeAccessor/CycleType/PickType
mixins landed.

Register / annotation resolution
* parametric_lifecycle.py: hoist `entity_instance` import out of
  TYPE_CHECKING so typing.get_type_hints resolves the
  Callable[[entity_instance], bool] annotation at operator registration
  (CycleDoorType, CycleWindowType, CycleStairType failed with NameError).
  Clarify the INTERFACE return contract on the picker entry-point so
  readers see why the gizmo step stays off the undo stack.

Framework callable contracts
* model/wall.py, door.py, window.py, stair.py: migrate `props_getter`
  and `element_checker` from bl_idname strings to bound classmethods
  on tool.Model / tool.Parametric. BaseParametricGizmoGroup.get_props
  expects a callable; the string form raised TypeError on first
  gizmo poll.
* model/door.py, model/stair.py: drop the dead `prop_path=` operator
  kwarg from create_arc_gizmo / create_icon_gizmo call sites. The
  framework helper blindly setattrs every kwarg onto the operator's
  OperatorProperties, but ToggleDoorSwing / ToggleStairProperty don't
  declare prop_path — the setattr raised mid-setup_element_specific_gizmos,
  so self.gizmo_door_type / self.lock_gizmo never got assigned and
  every subsequent draw_prepare tornadoed AttributeError. Nothing
  reads op.prop_path anywhere; the kwarg was dead data.

Dispatcher operators
* model/array.py: add EnableEditingParametric (the framework pen-icon
  dispatcher that routes to a per-feature edit operator by bl_idname
  string) and AddArrayFromFeatureEdit (binds the framework's array
  icon to bim.add_array on the current parametric draft).
* model/__init__.py: register both new operators.

Per-frame robustness
* drawing/gizmos.py: guard BaseParametricGizmoGroup.draw_prepare with
  is_setup_complete() — matches the existing guard in refresh() and
  in BaseSchematicGizmoGroup.draw_prepare(). Defense-in-depth: when
  any subclass's setup raises mid-way, draw_prepare now no-ops cleanly
  instead of per-frame AttributeError-tornadoing on whatever attribute
  the failed setup phase was meant to populate.
* model/decorator.py: guard ProfileDecorator.__call__ against
  context.active_object is None. The decorator is a per-frame
  viewport draw handler; deselecting or deleting the active object
  while it's installed crashed on obj.mode access. Treat None the
  same as "no longer in edit mode" — uninstall + fire the exit
  callback if present.
* geometry/data.py: ViewportData.load() populates `data` before
  flipping `is_loaded`, so a raise from cls.mode() no longer leaves
  the class flag-set but data-empty for subsequent reads.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:38 +02:00
Gorgious56 1b272c039d Refactor bim/module/drawing/gizmos — framework + icon infra
Three concerns bundled into one cohesive refactor of gizmos.py
(splitting them surgically requires intermediate commits with
duplicate same-named classes that Python can't parse):

1. Framework primitives — StaticTrisGizmoMixin + TexturedQuadGizmoMixin
   replace the older TrisGizmoMixin. New module-level helpers:
   _get_static_tris_shader / _get_static_tris_batch / clear_static_
   tris_cache for cached GPU batch reuse, _draw_outline_and_body for
   the shared outline-then-body render path, draw_tris_with_outline
   as the public wrapper. billboarded_at(world_pos, billboard_rot,
   scale) is the canonical billboard-matrix helper; should_flip_extend_
   arrow encapsulates the view-aware mirror decision for extend
   gizmos; get_warning_color_from_prefs reads the user's warning
   color.

2. Config classes — BaseValueGizmoConfig (shared visibility + dimension-
   text contract), CountGizmoConfig (array N indicator),
   DimensionGizmoConfig (length / height / depth labels), IconActionConfig
   (icon-only gizmos that invoke an operator on click). DimensionRenderer
   draws the actual numeric label using BLF.

3. Icon classes — each rewritten on StaticTrisGizmoMixin so they share
   the cached GPU batch + outline-then-body render path:
   GizmoLockOpen / GizmoLockClosed (replacing the single-state
   GizmoLock), GizmoArc, GizmoFillet, GizmoWallCornerIcon,
   GizmoWallTeeIcon, GizmoPen / GizmoValidate / GizmoCancel (the
   parametric-edit triad), GizmoPlus / GizmoMinus / GizmoTrash,
   GizmoArrayParent / GizmoArrayAll / GizmoArrayLayerIndicator (array
   context indicators with a small digit-rendering helper for the "xN"
   count label), GizmoMerge / GizmoSplit / GizmoUnjoin (wall-join
   icons), and GizmoMenu (textured-quad icon-action menu trigger).

The legacy TrisGizmoMixin, GizmoLock, and DimensionDrawConfig are
removed; downstream callers in subsequent PR4 commits swap to the
new mixin and config classes when their feature operators land.

CycleTypeMixin / PickTypeMixin / TypeAccessorBase live in
bim.parametric_lifecycle (previous commit). The three mixins are
re-exported from gizmos.py here so feature-module access via
``gizmo.<MixinName>`` keeps working until PR5 cleanup drops the
re-exports.

bim/module/drawing/__init__.py is updated in the same commit to
register the 11 new gizmo classes (GizmoLockOpen / GizmoLockClosed /
GizmoFillet / GizmoWallCornerIcon / GizmoWallTeeIcon / GizmoTrash /
GizmoArrayParent / GizmoArrayAll / GizmoArrayLayerIndicator /
GizmoUnjoin / GizmoMenu) — without that, the new classes exist in
gizmos.py but aren't usable as bpy gizmo types.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:38 +02:00
Gorgious56 b84be2e84c Add TypeAccessorBase + CycleTypeMixin + PickTypeMixin
Three operator mixins for type-selection ops on parametric features
(door type-cycle, window type-pick, stair type-cycle, railing
type-pick, roof type-cycle, etc.). Each shares the same contract:

* ``element_checker`` validates the active object is the expected
  IFC type
* ``props_getter`` resolves the BIM<Name>Properties group
* ``type_literal`` is the Literal type whose args drive the enum
* ``type_attr`` is the PropertyGroup field to read/write
* ``skip_element_check=True`` bypasses element validation (for
  operators that target a non-IFC context)

CycleTypeMixin shift-click reverses direction (forward by default).
PickTypeMixin opens a popup menu and routes the picked value
through execute() so F6 redo / EXEC_DEFAULT reach the apply path.
The PickType modal-handler dance waits for LEFTMOUSE release before
opening the menu when invoked mid-click (e.g. from a gizmo's
target_set_operator) so Blender's drag-through-pick gesture doesn't
commit an accidental item.

Ships standalone — the next commit's gizmos.py framework refactor
re-exports these names from bonsai.bim.parametric_lifecycle so
gizmo modules can spell ``gizmo.CycleTypeMixin`` / ``gizmo.PickTypeMixin``.
Concrete operator subclasses land in subsequent PR4 commits per
feature (door / window / stair / railing / roof).

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:38 +02:00
Bruno Perdigão d179c4415c Remove debug print 2026-06-11 18:34:38 +02:00
Bruno Perdigão cf34be07e1 Add more no headless test for snap 2026-06-11 18:34:38 +02:00
Bruno Perdigão 5f5e7de5f6 Merge tests into a single file 2026-06-11 18:34:38 +02:00
Bruno Perdigão bc07342354 Add test files and scripts 2026-06-11 18:34:38 +02:00
Bruno Perdigão 64dadac8d6 Initial implementation of tests for modal operators 2026-06-11 18:34:38 +02:00
Gorgious56 89b4072d6d Bonsai Makefile - pin deepdiff<9.1
deepdiff 9.1.0 added cachebox<6,>=5.2 as a direct runtime dep.
cachebox 5.2.3 only publishes macOS x86_64 wheels for macosx_10_12+,
incompatible with the macos py311 build's --platform macosx_10_10_x86_64.
The daily build's linux-wheel safeguard fires when the resulting
cachebox-*-manylinux_*.whl leaks into the macOS / windows wheels folder
(builds run on ubuntu-latest and cross-build via pip download --platform).

Pin deepdiff to <9.1 (resolves to 9.0.0, no cachebox transitive dep) as
the minimal hotfix. Long-term cleanup: bump the macos py311 platform tag
from 10_10 to 10_13 (matching py312/py313) and re-flag this line with the
standard \$(PYPI_PLATFORM) --only-binary=:all: pattern used by brickschema
and python-socketio.

Partly generated with the assistance of an AI coding tool.
2026-06-11 18:34:38 +02:00
Ryan Schultz ee3f49b111 Restore pre-aggregate selection on exit; deselect on unsupported profile
When override_mode_set_edit encounters an unsupported profile (Couldn't
import profile), deselect the object so Tab continues to cycle cleanly.

Also restores the selection that existed before entering aggregate mode
when finally tabbing out, via save/restore_previous_selection().
2026-06-11 18:34:38 +02:00
Ryan Schultz 73063cebf1 Deselect geometry after exiting item mode in aggregate context
Following the pattern from 586f9be077, deselect the active object after
exiting item mode so Tab continues to cycle cleanly. Also deselects
parametric LAYER1/LAYER2 items that cannot be edited directly, avoiding
the need to manually deselect before Tab-cycling out of aggregate mode.
2026-06-11 18:34:38 +02:00
Ryan Schultz 656e3bb4e8 Add select_similar to type attribute panels
In BIM_PT_type_attributes and BIM_PT_object_attributes (when
the active object is a type), attribute value buttons now use
"type.<Attr>" as the selector key so the operator finds
matching occurrences via their relating type rather than the
occurrence's own (often unset) attributes.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:38 +02:00
Ryan Schultz f1a4ea4207 Add clipboard copy to SelectSimilarContainer operator
After selecting objects in the same container, copy a `location="Name"`
filter query to the clipboard and report it — consistent with the same
behaviour in SelectSimilarType, SelectSimilarAggregate, SelectIfcClass,
and SelectSimilarMaterial.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:37 +02:00
Ryan Schultz 70ec460376 Fix #8128: Fix filter_elements skipping groups after a zero-result facet_list
When a `+`-separated filter group returns no results, `FacetTransformer.facet_list`
was skipping the reset of `has_additive_facet_in_current_list` because the reset
was inside the `if self.elements:` guard. The stale flag caused the next group's
`add_default_elements()` to bail out early, leaving its element set empty and
silently dropping every subsequent group from the result.

Move the flag reset outside the guard so it always fires regardless of whether
the group produced any results.
2026-06-11 18:34:37 +02:00
Ryan Schultz 51ba331226 Closes #8127: Add imperial location display to Placement panel
In the Placement panel, show Location and Rotation X/Y/Z
each on their own row beneath a header label. When the IFC
file uses imperial units, display a read-only feet-and-inches
label alongside each Location input field.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:37 +02:00
Ryan Schultz efcabed10d Format stair lengths using IFC length unit
Display general and calculated stair parameters (Width,
Height, Tread Run, Tread Rise, Length, etc.) formatted
to the IFC file's configured length unit rather than
raw numeric values.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:37 +02:00
carlopav 11154baa39 IfcCostSchedule PDF export with typst: fix bugs
Fixed a bug when a summary cost has no sum applied.
Added Currency in table header.
Cleanup.
Added guards for end summary.
2026-06-11 18:34:37 +02:00
Gorgious56 3e9b947b3f Cache opening previews + dissolve fill
DecorationsHandler now caches dissolved edges (mesh-keyed), world-space
draw payload, and GPUBatch objects with per-object epoch invalidation —
moving one wall doesn't wipe 50 opening caches. Object-mode dissolve
removes triangulation noise; 2-pass depth-test split dims occluded lines
instead of hiding them. Edit-mode behavior unchanged.

Also: disable viewport shadows for IfcFeatureElementSubtraction objects,
and wire DecorationsHandler.uninstall() into the model module's
unregister() so the new persistent handlers don't leak on addon disable.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:37 +02:00
Ryan Schultz 37456c9a1d Fix CardinalPoint not applied to all selected objects
EditAssignedMaterial propagated layer set usage attributes
to all selected objects but skipped this loop for profile
set usage. Add the same loop so CardinalPoint and
ReferenceExtent are copied to each selected object's
IfcMaterialProfileSetUsage on save.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:37 +02:00
Ryan Schultz 4e54f67cc0 Fix negative zero in imperial feet-inches parser
When the user enters `-0' - 10"`, Python parses feet as -0.0.
The check `feet < 0` is False for negative zero, so the sign was
silently dropped. Use math.copysign to detect it correctly.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:37 +02:00
Gorgious56 f9f756ae79 Add tests for decorator_cache + undo-resync dispatch
Two paired test files for the framework infrastructure landed
earlier in this PR.

test_decorator_cache.py (11 tests):
* The 4-hook invalidation list (depsgraph_update_post + undo_post +
  redo_post + load_post) is symmetrically managed by
  install_decorator_cache_handlers / uninstall_decorator_cache_handlers.
  A future edit that drops a hook from one side without the other
  would land as a Blender segfault when a cached bpy.types.Object
  ref outlives its underlying ID block — the regression must surface
  as a test failure first.
* install is idempotent (calling twice doesn't double-register).
* uninstall when not installed doesn't raise.
* The bump handler accepts Blender's variadic args.
* The depsgraph predicate gates correctly: bumps on Object geometry
  or transform updates, silently skips on Material / NodeTree / Image
  updates (which would otherwise rebuild every cache on every node
  edit).
* TokenCache.get_or_compute short-circuits on key+token match and
  recomputes when the token bumps.

test_undo_resync_parametric_drafts.py (3 tests):
* UNDO_REGENERATORS keys must all be in tool.Parametric.EDIT_TYPES.
  A typo would silently no-op on Ctrl+Z, restoring the desync the
  helper is meant to prevent.
* The dispatcher skips objects with no active parametric edit
  (undo_post fires for every undo, most of which touch zero drafts).
* The dispatcher silently skips parametric types that have no
  UNDO_REGENERATORS entry (door / window / array are IFC-derived
  with no draft preview mesh — they don't need a regenerator).

Mocks use spec=bpy.types.Depsgraph / spec=bpy.types.DepsgraphUpdate
/ spec=tool.parametric.ParametricObject so typos in mocked-attribute
access fail loudly (CLAUDE.md test discipline).

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:37 +02:00
Gorgious56 26502a1a2a Fix wall split: preserve door/window fill rel
Splitting a wall through a door orphaned the door (door.FillsVoids
became empty). The fill rel was being reassigned by setting its
RelatedBuildingElement slot — schema-wise that's the filling slot, not
the wall slot — so when remove_feature deleted the old opening it
also cascade-removed the rel. Transferring via RelatingOpeningElement
keeps the rel pointing at the new opening so the door stays
associated. Pre-existing bug from 5a6476a57, surfaced by ef144dce2.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:37 +02:00
Gorgious56 05812d089a Fix wall split: keep straddling openings on both walls
DumbWallJoiner.split assigned openings by projecting the opening's
centre-point onto the wall axis, so any opening whose footprint
straddled the cut was silently dropped from whichever wall its centre
missed. Now the full axis-projected extent (via ifcopenshell.geom.
create_shape) drives the assignment; for filled openings whose void
straddles the cut, a pure-void copy is added back to the neighbour
wall so its body is also cut.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:37 +02:00
Gorgious56 4c2100593b DRY tag-redraw-3D-viewports loops via tool.Blender.update_all_viewports
Five inline copies of the same defensive pattern lived across
``tool/parametric.py``, ``bim/parametric_lifecycle.py``,
``bim/module/model/preview_base.py`` (twice), and as a near-twin
in ``tool/blender.py:update_all_viewports`` itself.

``tool.Blender.update_all_viewports`` already covered the
``tag_redraw`` job but used an ``assert context.screen`` that would
raise during background-mode operators or early-load_post calls
where ``screen`` legitimately is None. Relax to a defensive
``getattr(context, "screen", None)`` + silent return so the helper
fits every caller's needs, then collapse the 4 inline copies to
single calls.

Net -9 LOC. The helper now describes its contract ("silent no-op
when no screen attached") rather than naming specific callers, so
moving a caller doesn't rot the docstring.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:36 +02:00
Gorgious56 dbb6cff723 Add parametric-draft undo-resync registry
Ctrl+Z / Ctrl+Shift+Z on an in-progress parametric draft (wall /
stair / roof) used to leave the preview mesh frozen in its
pre-undo shape — the IFC mutation rolls back but the bmesh built
from draft props doesn't repaint.

Add a registry of per-type regenerator functions
(``UNDO_REGENERATORS``) that re-build each type's preview mesh
from its current props. The dispatcher
``resync_parametric_drafts_after_undo`` walks all objects, skips
any without an active parametric edit, looks up the regenerator
by feature name, and calls it. Tagged 3D viewports for redraw.

Types without an entry (door / window / railing / etc.) are
intentionally absent — they're IFC-derived, so the undo's
representation rollback + next-frame refresh already repaints
correctly without a draft-side regenerator.

Undo/redo wiring is self-installed by
``bonsai.bim.parametric_lifecycle``: a ``@persistent``
``_resync_on_undo`` callback dispatches into the registry, and
``install_parametric_lifecycle_handlers()`` /
``uninstall_parametric_lifecycle_handlers()`` append/remove it
from ``bpy.app.handlers.undo_post`` and ``redo_post``.
``bim/__init__.py``'s ``register()`` calls the install function
*after* the central ``handler.undo_post`` / ``redo_post`` appends
so the regenerators see restored IFC state — ``bpy.app.handlers``
fire in append order. ``handler.py`` itself stays ignorant of the
parametric subsystem. The lazy function-local imports in each
regenerator break the addon-load cycle —
``bonsai.bim.parametric_lifecycle`` loads before
``bim/module/model/*``.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:36 +02:00
Gorgious56 aba9986628 Refactor bim/parametric_lifecycle — drift triad + Cancel polish
Three changes to the shared Enable/Finish/Cancel mixins:

1. Always-on drift triad on ParametricEditMixinBase. The base now
   provides ``_handle_drift_on_enable`` / ``_handle_drift_on_finish``
   / ``_handle_drift_on_cancel`` classmethods, called from the
   per-mixin ``_enable_one`` / ``_finish_one`` / ``_cancel_one``.
   Pre-edit Blender-side translations commit to IFC on Enable
   (apply_scale=False — only translation/rotation, not the user's
   accidental scale), in-edit drag commits on Finish (apply_scale=True),
   and Cancel restores the committed IFC placement via
   ``restore_or_rebaseline_placement``. Prevents the
   "uncommitted drag disappears on Finish" and "preview snaps back
   on Cancel" UX bugs.

2. ``_ParametricEditMixinBase`` renamed to ``ParametricEditMixinBase``
   (public). Per-feature mixins that need to subclass directly
   (e.g., when neither FeatureModifier nor PathPreserving fits)
   can do so without reaching into a private name.

3. ``_update_modifier_bmesh`` (PathPreserving) renamed to
   ``_restore_viewport_after_cancel``. The old name was inaccurate
   for subclasses that load a different IFC representation on
   Cancel rather than rebuilding a bmesh preview from props.

Plus two polish changes:

* ``_mark_type_thumbnail_dirty`` helper on the base centralises the
  ``ifcopenshell.util.element.get_type`` + thumbnail-mark pattern
  that both mixins repeated inline.
* ``FeatureModifierEditMixin._cancel_one`` and
  ``PathPreservingEditMixin._cancel_one`` wrap the restore in
  ``try/finally`` so ``props.is_editing = False`` flips even on
  partial restore failure. Without this, a Cancel that raised
  mid-restore would leave the user locked out of the edit lifecycle.
* ``PathPreservingEditMixin._finish_one`` / ``_cancel_one`` skip the
  pset commit + viewport rebuild when the draft equals the stored
  pset (no-op Enable→Finish round-trip should not pollute the
  representation list or burn an undo entry).

``FeatureModifierEditMixin._finish_one`` now routes the pset commit
through ``tool.Pset.write_bbim_data`` instead of inlining the
``createIfcText(json.dumps(...))`` + ``ifcopenshell.api.pset.edit_pset``
dance. Two test assertions updated to match.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:36 +02:00
Gorgious56 ed6530f68b Decompose bim/handler.py load_post + install cache + discard hooks
Three concerns folded into ``load_post`` argue for separation:

1. Save-file invariants every load must re-establish (msgbus
   subscription, owner-settings, thumbnail cache, draft-flag healing,
   blend-warning flag, H5 lock probe).
2. User-preference-driven UI setup (toolbar, workspace, viewport
   shading, panel hijack, snap defaults).
3. Viewport overlay sync (every decorator's install/uninstall).

Pull each into its own function (``_apply_save_file_invariants`` /
``_apply_user_preferences`` / ``_install_viewport_overlays``). The
``load_post`` callback becomes a 3-line orchestrator. Each phase
is independently call-able from tests and from PR4 features that
need to re-trigger one phase without the others.

Two new hooks land with the decompose:

* ``tool.Parametric.heal_stale_edit_flags()`` + ``discard_pending_previews(scene)``
  fire in ``_apply_save_file_invariants``. The first clears
  object-level ``BIM<Name>Properties.is_editing`` flags that lost
  their backing IFC element across a load; the second clears
  scene-level ``BIMPreviewProperties.<x>.is_active`` so saved
  preview state never resurfaces with no UI to interact with it.

* ``install_decorator_cache_handlers`` / ``uninstall_decorator_cache_handlers``
  wrap the decorator install/install pass in
  ``_install_viewport_overlays``. The bump handlers append to
  ``depsgraph_update_post`` + ``undo_post`` + ``redo_post`` +
  ``load_post`` so the previous commit's ``TokenCache`` in
  ``tool.System.get_decoration_data`` finally invalidates on
  structural scene changes.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:36 +02:00
Gorgious56 55d5e15d6a Add bim/module/model/preview_base module
Shared helpers for Bonsai's Scene-level parametric preview flows.
Two PR4 features will consume this — MEP bend preview and wall
fillet preview — both following the same shape:

    Enable<X>Preview   — populates draft on Scene.BIMPreviewProperties.<x>
    Gizmo<X>Preview    — polls on is_active, surfaces tunable widgets
    <X>PreviewDecorator — GPU lines while is_active is True
    Finish<X>Preview   — bpy.ops.bim.<verb>(...) with draft kwargs
    Cancel<X>Preview   — pure state reset

The module hosts the cross-cutting accessors (``get_preview_props``,
``is_preview_active``), lazy-closure factories for gizmo dimension
callbacks (``make_props_callback`` / ``make_dim_getter`` /
``make_dim_setter`` — defensive against missing scene / freed RNA
struct on file open / undo), the Enable-time IFC-placement sync
(``sync_uncommitted_moves``), and the Esc + load_post discard
machinery (``PREVIEW_CANCEL_OPS`` registry, ``try_cancel_active_preview``,
``discard_pending_previews``).

Ships standalone — the consumer features land in PR4 (preview
PropertyGroups, Enable/Finish/Cancel operators, gizmo groups,
decorators, Esc keymap binding). All accessors are defensive
against missing PropertyGroups / operators on v0.8.0 — calling
``discard_pending_previews(scene)`` from the next commit's
load_post hook is a no-op until PR4 attaches BIMPreviewProperties.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:36 +02:00
Gorgious56 f7db539fcd Wrap tool.System.get_decoration_data with TokenCache lookup
System decoration draws on every viewport refresh — the
``_build_decoration_data`` body walks every distribution element,
resolves connected ports, builds the vert/edge arrays for the GPU
batch. A bare call per frame burns time on an unchanged scene.

Add a single-entry cache keyed on ``(decorator_cache_token,
id(decorated_elements_set))``. Reads short-circuit when neither
component moved:

* ``decorator_cache_token`` from ``bim.decorator_cache`` invalidates
  on depsgraph / undo / redo / load via the bump handler.
* ``id(decorated_elements_set)`` invalidates when
  ``SystemDecorationData.load()`` reassigns the set (e.g. when the
  user changes the set of decorated systems via the panel).

The handler that bumps the token is installed in the next commit
(bim/handler.py decompose). Until then the token stays at 0, so
the cache only hits when ``id()`` also matches — degraded behaviour
during the bisect window but not incorrect.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:36 +02:00
Gorgious56 3f9ecbeda1 Add bim/decorator_cache module — TokenCache + handler primitives
New helper module for POST_VIEW decorators. Exports:

* ``get_decorator_cache_token()`` — global int counter consumers
  include in their cache key so the value invalidates on structural
  scene changes.
* ``_bump_decorator_cache_token()`` — ``@bpy.app.handlers.persistent``
  callback that increments the token. Gates on the depsgraph payload
  so animation playback / driver evaluation doesn't churn the token.
* ``install_decorator_cache_handlers`` / ``uninstall_…`` — idempotent
  append / remove against depsgraph_update_post + undo_post + redo_post
  + load_post. Called once from ``bim.register`` / ``unregister``.
* ``TokenCache[T]`` — single-entry memoiser keyed on ``(caller_key,
  token)``. Cached ``bpy.types.Object`` references can't outlive the
  underlying ID blocks because any depsgraph / undo / load bumps the
  token and forces a recompute.

This commit ships the module standalone. The next commits in this
PR wire it: tool/system.py adds the cache wrap on get_decoration_data
and bim/handler.py installs the bump callbacks. Until both land,
the module is intentionally dead code — keeps the diff narrow and
the commit history bisectable.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:36 +02:00
Gorgious56 731b057892 Fix latent runtime bugs + ty annotations surfaced by CI
Five code paths in slim PR2 referenced symbols that don't exist in
v0.8.0's bim layer, raising at first call. Plus three type
annotations that ty flagged as unresolved.

1. tool/system.py:get_decoration_data — drop the cache layer that
   keyed on a token from a bim/decorator_cache.py module. The cache
   is dead-or-broken in slim: the depsgraph bump handler that would
   invalidate the token lives in PR3's bim/handler.py decompose, so
   the token stays at 0 forever. Either the cache never hits
   (decorated_elements rebuilt → new id() per call) or returns
   stale data (list reused). Revert to direct
   `_build_decoration_data()` calls. PR3 reintroduces the cache
   atomically: decorator_cache module + handler install + cache
   wrap + tests. Keeps `_build_decoration_data` extraction
   (cleaner than v0.8.0's monolithic version regardless of cache).

2. tool/spatial.py — add `get_host_element` + `get_host_wall`.
   The interface stubs in `core/tool.py:1037-1038` were declared
   but never implemented. `tool/duplicate.py:99` (object duplication
   with fills) and `tool/model.py:1260` (array per-child opening
   mirror) call these and would raise AttributeError.

3. tool/model.py:recreate_wall — drop the fillet-corner branch
   that function-locally imports `regenerate_fillet_corner_wall`
   from `bim/module/model/wall`. The function lands with PR4; fall
   through to the straight-extrusion path preserves v0.8.0
   behaviour for fillet walls until then. Tag FIXME(PR4).

4. tool/model.py — drop `get_pipe_segment_props` /
   `get_duct_segment_props` accessors. Their return types reference
   `BIMPipeSegmentProperties` / `BIMDuctSegmentProperties` which
   land with PR4's prop.py; calling either accessor on v0.8.0 would
   AttributeError on `obj.BIM<X>SegmentProperties`. Zero callers in
   slim — PR4 reintroduces both accessors together with the
   PropertyGroups they wrap. Also drops the matching TYPE_CHECKING
   imports.

5. tool/blender.py:557 — `Mapping[type[ViewportDecorator], bool]`
   needs the qualified `Blender.ViewportDecorator` because the
   annotation is on a method INSIDE the same nested class; the
   bare name doesn't resolve at type-check time.

6. core/tool.py Surveyor — drop the `obj: "bpy.types.Object"` /
   `z: float` / `-> float` / `-> None` annotations on
   `get_z_rotation` / `set_z_rotation`. The `@interface` decorator
   wraps each method as `classmethod(abstractmethod(...))` at
   import time, but ty doesn't track the wrap and flags every
   call site as `missing-argument` plus the `pass` body as
   `empty-body` against the declared return type, plus the
   `bpy.types.Object` forward-ref as `unresolved-reference`.
   Reverting to v0.8.0's untyped style (matching the sibling
   `get_absolute_matrix(cls, obj)` stub) clears six ty errors at
   the cost of zero runtime semantics — the abstract stubs only
   serve as registry markers, concrete `tool.Surveyor.*` carries
   the real signatures.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:36 +02:00
Gorgious56 7d77ea032c Add addon-load smoke test pinning register/unregister cycle
Surfaces any regression in:

* the modules dict in bim/__init__.py (added a folder, forgot the entry)
* PointerProperty wiring on bpy.types.{Scene,Object,...}
* registry-driven GizmoPreferences<Name> auto-registration in
  tool.Parametric.iter_gizmo_preference_classes
* bpy.app.handlers append/remove balance
* every register()/unregister() across the 45+ feature modules

as a single PASSED/FAILED test instead of the silent "addon failed to
enable" users encounter in a fresh Blender. Paired with the existing
test_parametric_registry.py contract tests, this catches both the
registry-shape regressions (operators/PropertyGroups/predicates) and
the registration-mechanics regressions (PointerProperty types not
registered before their owners).

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:36 +02:00
Gorgious56 8669f53fc1 Fix tool.Parametric to ship safely on v0.8.0 bim layer
Three corrective fixes folded into one commit. All surface as
addon-load / save-time exceptions on v0.8.0's bim layer because
PR2's tool.Parametric refactor over-committed to the PR4 contract.

1. iter_gizmo_preference_classes — the previous implementation
   returned only the shared GizmoPreferencesFeature class. v0.8.0's
   bim/ui.py declares PointerProperty fields ('door', 'window', ...)
   on GizmoPreferences that point at per-feature
   GizmoPreferences<Name> classes; those must be registered BEFORE
   GizmoPreferences itself. The shared-class-only return broke
   addon registration with:
      'door' PointerProperty could not register (see previous error)
   Restore the v0.8.0 per-feature lookup (iterate EDIT_TYPES, look
   up each GizmoPreferences<Capitalize(name)> on ui_module) and
   keep the shared-class lookup as forward-compat. Tag FIXME(PR5).

2. EDIT_TYPES — drop the array / pipe_segment / duct_segment
   entries from the registry. Their bim.finish_editing_<name>
   operators land with PR4. Registering them in PR2's EDIT_TYPES
   without the operators makes auto-commit-on-save dispatch a
   non-existent finish_op for any object whose
   BIM<Name>Properties.is_editing flag is True, raising:
      RuntimeError: 'bim.finish_editing_array' must be a registered
      tool.Ifc.Operator subclass for undo-safe IFC mutation
   PR4 re-adds the three entries together with their operators.
   Tag FIXME(PR4).

3. tool.Blender.Modifier shim block — upgrade the prose comment to
   a formal FIXME(PR5) marker so the PR5 cleanup sweep finds it via
   grep alongside every other tagged shim site.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:36 +02:00
Gorgious56 3eafc9dc49 Extract bim/ifc + tool/cad helpers referenced by PR2
Fixes addon-load ImportError that surfaces when tool/geometry.py
and tool/model.py (extracted in C8 / C9) reference symbols that
don't exist on v0.8.0:

* bim/ifc.py: get_cache_or_detect_lock — IfcStore.get_cache
  variant that tracks the multi-instance-cache-locked-by-other-
  process flag, sets it on PermissionError, clears it (along with
  the dismiss flag) on subsequent success. Used by
  tool.Geometry.* to gate IFC cache reads without crashing when
  another Blender instance holds the cache lock.
* tool/cad.py: WELD_TOLERANCE constant + paired CAD helpers
  (auto-detect-curves vertex precision, polyline normal helpers,
  etc.) used by tool.Model.* + by the parametric model operators
  that land in PR4.

Both modules had zero upstream commits since the gizmos-8088 fork
point — safe bulk extraction. PR4 has no caller-line work for
either file (the additions are pure additions, no existing API
removed); the v0.8.0 callers of get_cache_or_detect_lock and
WELD_TOLERANCE are the PR2-scope files that needed them.

Generated with the assistance of an AI coding tool.
2026-06-11 18:34:35 +02:00
Gorgious56 79a4ce648f Add tool.Blender.Modifier backward-compat shims
The previous commit moved is_<type> predicates off tool.Blender.Modifier
onto tool.Parametric, and earlier C4 moved the Array helper bag off
tool.Blender.Modifier.Array onto tool.Array. PR4 will migrate every
caller; this commit keeps the OLD entry points alive as thin delegates
so PR2 ships without breaking ~30 caller sites that still spell the
old API in v0.8.0:

* tool.Blender.Modifier.is_door / is_railing / is_roof / is_stair /
  is_wall / is_window — delegate to tool.Parametric.is_<type>.
* tool.Blender.Modifier.Array.bake_children_transform / constrain_
  children_to_parent / get_all_children_objects / get_all_objects /
  get_children_objects / get_modifiers_data / remove_constraints /
  set_children_lock_state — delegate to tool.Array.<same name>.

These shims are removed in PR5's cleanup commit once PR4 has rewritten
the call sites in bim/import_ifc.py, bim/module/geometry/operator.py,
bim/module/geometry/data.py, bim/module/model/array.py + the per-feature
operators (door, wall, window, railing, roof, stair, ui).

Generated with the assistance of an AI coding tool.
2026-06-11 18:32:48 +02:00
Gorgious56 8c9fcfd5e4 Refactor tool.Parametric — feature registry + lifecycle hooks
tool.Parametric becomes the central registry for Bonsai's parametric
features (wall, slab, door, window, railing, roof, stair, plus
mep-segment variants). Each feature registers a ParametricObject spec
declaring its enable/finish/cancel op names, props accessor, regen
callback, and is_element_type predicate.

Public surface:

* tool.Parametric.WALL / SLAB / DOOR / WINDOW / RAILING / ROOF /
  STAIR / PIPE_SEGMENT / DUCT_SEGMENT — typed accessors per feature.
* tool.Parametric.is_wall / is_door / is_window / is_railing /
  is_roof / is_stair — element-type predicates that move off
  tool.Blender.Modifier into the parametric registry. The next
  commit adds backward-compat shims on tool.Blender.Modifier so
  v0.8.0 callers keep working.
* tool.Parametric.is_object_editing(obj) — returns the registered
  feature an object is currently editing, or None.
* tool.Parametric.run_bim_op(op_name) — invoke a parametric op by
  bl_idname.
* tool.Parametric.heal_stale_edit_flags — clear is_editing flags
  on file load so a saved-mid-edit project doesn't leave gizmos
  poll-locked.
* supports_build_edit_lifecycle field on ParametricObject — declares
  whether the feature implements the build/edit/cancel triad.

The previous bare `print(f"Bonsai: commit of {obj.name!r} via
{finish_op} failed: {e}")` exception-handler is replaced with
logger.warning(..., exc_info=True). Same channel (Bonsai configures
logging to the Blender console at WARNING level), strictly more
information (full traceback), correct idiom for an error-path
message. A second logger.warning is added for parametric predicate
failures, also exception-handler scope.

Generated with the assistance of an AI coding tool.
2026-06-11 18:32:48 +02:00
Gorgious56 8cf6d5a001 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-06-11 18:32:48 +02:00
Gorgious56 7bb76660e6 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-06-11 18:32:48 +02:00
Gorgious56 6c4414aa4e 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-06-11 18:31:51 +02:00
Gorgious56 4a70250c68 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-06-11 18:31:51 +02:00
Gorgious56 3555c6effd 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-06-11 18:31:51 +02:00
Gorgious56 c0e2ff7298 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-06-11 18:31:51 +02:00
Gorgious56 80048c11a0 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-06-11 18:31:51 +02:00
Gorgious56 a0d739d995 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-06-11 18:31:50 +02:00
Gorgious56 ffb2b90089 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-06-11 18:31:50 +02:00
Gorgious56 ccbfba89b9 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-06-11 18:31:50 +02:00
Gorgious56 4bbcb59259 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-06-11 18:31:50 +02:00
Gorgious56 38fab26000 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-06-11 18:31:50 +02:00
Gorgious56 d1d6b46ce2 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-06-11 18:31:50 +02:00
Gorgious56 49c7df0cb1 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-06-11 18:31:50 +02:00
Richard Brice 3559d23f81 Updates alignment api. Fixes bugs authoring semantic-only alignment 2026-06-11 18:31:50 +02:00
Richard Brice 852311279d Simplifies line and circle parent curves and parent curve normalization 2026-06-11 18:30:10 +02:00
Richard Brice 54dd0b5448 Fixes bug computing cross slope 2026-06-11 18:30:10 +02:00
Ryan Schultz 9169e8ed24 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-06-11 18:30:10 +02:00
Bruno Postle 8013fd5902 Quote {id} placeholders in examples (issue #8101)
Shell {} expressions require quoting
2026-06-11 18:30:10 +02:00
Gorgious56 9df91d668b Add lifecycle-mixin tests + predicate-total registry guard
test_parametric_lifecycle.py covers the door/window/railing/roof
state-transition contracts (enable/finish/cancel; no-op on
non-matching elements; draft preserved on finish-time failure)
that the registry smoke test never exercised.

test_parametric_registry.py gains a check that every is_<name>
predicate stays total (never raises on a non-matching IFC entity)
— a raising predicate would break the save path for unrelated
types. Also rewrites the gizmo-prefs check to read __annotations__
instead of hasattr, which depended on Blender registration timing.

Generated with the assistance of an AI coding tool.
2026-06-11 18:30:10 +02:00
Gorgious56 2456808b67 Defer mathutils imports in stair gizmo tests
Aligns with the test/bim/ convention: heavy imports go inside test
functions so the autouse _require_real_bpy fixture skips cleanly
when bpy is mocked, rather than module-level imports failing at
collection time and erroring out the whole file.

Generated with the assistance of an AI coding tool.
2026-06-11 18:30:10 +02:00
Gorgious56 7d779df981 Add BONSAI_TEST_ARGS env-var fallback to runpytest.py
PowerShell and some wrapper scripts on Windows occasionally strip
or reorder the `--` separator before Blender sees it, dropping the
pytest args into Blender's positional file-load slot ("File format
is not supported"). The env var carries the same args via a
shell-evaluation-free channel. Default `--` path is byte-identical
to the pre-change behaviour.

Generated with the assistance of an AI coding tool.
2026-06-11 18:30:10 +02:00
Gorgious56 872dd26e1c Sweep docstrings for rot-prone references
Docstrings naming sibling methods, private helpers, test files, or
historical symbols silently go wrong on rename. Strip Sphinx :meth:
/ :class: / :func: / :attr: markup that mostly added noise (no
Sphinx in this project), and rewrite five docstrings that cited
specific test paths or private hooks to describe the behaviour
instead.

Generated with the assistance of an AI coding tool.
2026-06-11 18:30:10 +02:00
Gorgious56 4df946be71 Drop save-time parametric-edit confirm dialog
The dialog's only outcomes were "Apply & Save" (same as silent save)
or "Cancel" (same as not saving) — net friction with no actual choice.
Auto-commit stays as the safety net; the count now suffixes the
existing save-success report so it isn't immediately overwritten.

Generated with the assistance of an AI coding tool.
2026-06-11 18:30:10 +02:00
Gorgious56 74906ac9fe Prioritize smaller distance gizmos in selection
When two GizmoDimension hit regions overlap (a short dimension
nested inside a longer one along the same axis), the larger one
used to win because hit boxes are scaled by world-space length —
the long box fully contains the short one, leaving the short
gizmo unreachable. The larger gizmo stays clickable at its
exposed ends, so smaller-wins is the right UX default.

Sets self.select_bias = -self._dimension_length inside
GizmoDimension.set_dimension_length. The smaller gizmo writes a
less-negative depth value in the GPU select buffer and wins the
tie-break. select_bias is unused elsewhere in the codebase, so
icon and arrow gizmos keep bias=0 and are unaffected (icons
correctly still win against dimensions, since 0 > -length).

Adds test/bim/module/drawing/test_dimension_gizmo_priority.py
with 5 cases: direct ordering, monotonicity across length ranges,
abs() handling for signed dimensions, and NaN/Inf safety.

Generated with the assistance of an AI coding tool.
2026-06-11 18:30:10 +02:00
Gorgious56 ddd9b4fa23 Simplify pending edit popup text 2026-06-11 18:30:09 +02:00
Gorgious56 5d6878c321 Fix set_icon_gizmo_position so billboard ignores object rotation
set_icon_gizmo_position computed
``mw @ (Translation @ billboard_rot @ Scale)`` — the object's world
matrix was applied AFTER the billboard rotation, so any non-trivial
object rotation (e.g. a wall rotated in plan, a stair rotated to
match a corridor) carried over into the icon's transform and tilted
it edge-on to the camera instead of facing it.

Switch to ``billboarded_at(world_pos, billboard_rot, scale)`` where
``world_pos = mw @ local_pos``: translate to world space first, then
apply the billboard rotation independently of the object's rotation.
This matches the manual pattern the base class's
``update_editing_gizmos`` already uses for validate/cancel/cycle for
exactly this reason.

Drops the now-stale workaround docstring on
``GizmoWallEdition._update_icon_row_extras`` that documented why it
bypassed ``set_icon_gizmo_position`` — the helper does the right
thing now.

Adds ``test/bim/module/model/test_stair_gizmos.py`` as the regression
guard: parametrised over six rotation angles, asserts that the rotation
part of the resulting matrix equals ``billboard_rot`` (no contribution
from ``mw``'s rotation) and that the translation lands at
``world_pos``. Also exercises ``set_icon_gizmo_position`` end-to-end via
a stub gizmo to catch the exact shape of the previously-broken call
site.

Generated with the assistance of an AI coding tool.
2026-06-11 18:30:09 +02:00
Gorgious56 95a31b49ec Add wall parametric editing and gizmos
Walls gain in-viewport parametric editing matching the door/window/stair
UX: drag handles for length, height, slope (x-angle), layer baseline
cycle, plus cursor-anchored quality-of-life operators (split at cursor,
extend to cursor, extend height, rotate 90, toggle openings) and
two-object state-machine gizmos (unjoin / merge / join-corner /
extend-to-wall / extend-vertically / add-opening).

Wall enters tool.Parametric.EDIT_TYPES, so save-time auto-commit,
GizmoPreferencesWall registration, and the in-progress-edit predicates
all light up automatically through the registry plumbing landed two
commits back.

The three-layer commit model (drag -> BIMWallProperties -> bmesh
preview -> Finish -> single ifc.run) means dragging a handle through
hundreds of intermediate values produces zero extra IFC entities. A
no-op enable->finish round-trip is byte-identical. The snapshot diff
in FinishEditingWall skips unchanged params.
_commit_active_wall_edit_if_any ensures cursor-anchored operators see
committed geometry, not the draft preview box.

Also lands the `prompt_auto_commit_parametric_edits` BoolProperty on
BIM_ADDON_preferences (consumed by the auto-commit dialog landed in
the framework commit) and refactors
`draw_{door,window,stair}_gizmo_parameters` into a shared
`_draw_parametric_gizmo_parameters` helper that the new
`draw_wall_gizmo_parameters` reuses. This commit and the framework
commit are stacked - the framework commit references the BoolProperty
defined here, so they must land together.

Tests cover pure math (core/test_model.py), DimensionGizmoConfig text
formatter, GizmoWallExtendVertically.poll() preconditions, and the
refresh_post_commit cache-invalidation regression. BDD scenarios in
model.feature cover the edit triad, auto-commit on save, and the
two-object gizmos. Documentation added to creating_walls.rst.

Generated with the assistance of an AI coding tool.
2026-06-11 18:30:09 +02:00
Gorgious56 b36bdf4130 Fix dead duplicates and misleading import comments
Three small post-landing cleanups against the parametric framework commit:

* core/model.py had `are_axes_collinear` and `closest_endpoint_midpoint`
  each defined twice — Python silently kept the second copy, the first
  was dead code. Removed the dead copies; runtime behavior unchanged
  (the live versions were already the kept ones).
* bim/__init__.py's `_parametric_gizmo_preference_classes` docstring
  named the wrong link in the import chain (`tool.blender → bim.ifc`).
  The real chain is `tool/ifc.py` (and ~6 other tool/* modules) which
  import `from bonsai.bim.ifc import IfcStore` at module load. Updated
  docstring to cite that root cause and the architectural fix (move
  `IfcStore` out of `bim/`).
* tool/blender.py's `from bonsai.bim.ifc import IFC_CONNECTED_TYPE`
  carried a 5-line comment claiming it was "lazy" to avoid a circular
  load. The import sits inside an `if TYPE_CHECKING:` block with
  `from __future__ import annotations` — it never runs at runtime
  regardless. Comment removed; the TYPE_CHECKING guard is
  self-explanatory.

Generated with the assistance of an AI coding tool.
2026-06-11 18:30:09 +02:00
Gorgious56 fb70c64138 Add tool.Parametric registry and lifecycle mixins
Establish a single source of truth for parametric element types (door,
window, stair, railing, roof). tool.Parametric.EDIT_TYPES drives:
- BIM<Name>Properties PointerProperty attachment via the registry
- GizmoPreferences<Name> class registration in bim/__init__.py
- save-time auto-commit of pending draft edits
- the refresh_post_commit epilogue called from IfcStore after every IFC
  mutation, which fixes the stale-header bug where in-place hotkey
  mutations (S_E / C_E) left BIMModelProperties and the gizmo cache
  pointing at obsolete values.

Refactors door/window/railing/roof onto shared mixins from
bim/parametric_lifecycle.py (FeatureModifierEditMixin and
PathPreservingEditMixin); stair gets the lock-gizmo refactor and
frame-cache integration. Behavior preserved.

Adds BaseParametricGizmoGroup._prime_frame_caches so the parametric
gizmos stop re-deriving preferences, view direction, and billboard
rotation per frame; reorders poll() to short-circuit on the cheapest
predicate first. Adds the icon library + BillboardingGizmoGroupMixin
that the wall feature in the next commit will consume.

Generated with the assistance of an AI coding tool.
2026-06-11 18:30:09 +02:00
Gorgious56 0413be2c3f Fix 8077 : Fix SHIFT + D with non-ifc object selection
When a project has a ifc file associated, selecting non-ifc objects and duplicating them with SHIFT + D now correctly both duplicate them, keep the new objects selected and starts the transform modal. IFC objects behaviour is unaffected.
2026-06-11 18:30:09 +02:00
Geert Hesselink 69ae113434 Fix lint failures and add missing pyparsing dependency (#8048)
* unblock voxel schema loading, add test for express

* Apply black formatting

* Fix lint failures and add missing pyparsing dependency

* align ty -> 0.0.34
2026-06-11 18:30:08 +02:00
Thomas Krijnen 1f6c467c88 Change default value of assume_asset_uniqueness_by_name #8045 2026-06-11 18:29:12 +02:00
Thomas Krijnen 295c7d801c arrange polygons: limit width ratio when merging boxes 2026-06-11 18:29:12 +02:00
Ryan Schultz 1d9df1d90a Fix #8056 - Dimensions with CustomUnit" = "Inches - Fractional" should not show 0. 2026-06-11 18:29:12 +02:00
Thomas Krijnen 13bb8fbb98 arrange polies: don't allow snapped point paths to cross non-containing other rect axes 2026-06-11 18:29:12 +02:00
Thomas Krijnen 55d7c24dc8 Fix temporary solution storage in arrange polygons 2026-06-11 18:29:12 +02:00
Thomas Krijnen 3a14786a5b Calculate box-width as orthogonal distance; aabb code for segment intersection (disabled) 2026-06-11 18:29:12 +02:00
Thomas Krijnen 10f93545da Arrange polies: reorder segment to exterior insertion based on length 2026-06-11 18:29:12 +02:00
Thomas Krijnen 7a901c1fce Reduce log noise on materials without styles #7947 2026-06-11 18:29:12 +02:00
Thomas Krijnen 6fffe33da1 arrange polies, fuse boxes only when obb also overlaps 2026-06-11 18:29:11 +02:00
Ghesselink 2dbb8c59e3 Apply black formatting 2026-06-11 18:29:11 +02:00
Ghesselink aa053bd52c unblock voxel schema loading, add test for express 2026-06-11 18:29:11 +02:00
Thomas Krijnen 57d1feaba8 arrange polies: try connect to closest point when extension and projection both do not work 2026-06-11 18:29:11 +02:00
Thomas Krijnen 98a897dd34 arrange polies performance: retain input poly provenance while subdividing; insert into arrangement_2 in batches 2026-06-11 18:29:11 +02:00
Thomas Krijnen 3b4cff838e arrange polies: only subdivide segments that correspond to input poly segments 2026-06-11 18:29:11 +02:00
Thomas Krijnen b7a329c9bc arrange polies: apply triangle elimination in both algo 1 and 2 2026-06-11 18:29:11 +02:00
Thomas Krijnen 15574f78ac arrange polies: lower iou to 45% 2026-06-11 18:29:11 +02:00
Richard Brice 820077a94b Removes unnecessary operations when combining horizontal and vertical placement matrices for alignment 2026-06-11 18:29:11 +02:00
Thomas Krijnen cd51c3ae85 arrange polygons: debug output point and annotate self intersecting polies; fix snapping distance check and fallback; tweak max snap to exterior distance; accept non-simple polies - likely touching without edge overlap; write representative points to debug output; properly apply algo 1 fallback; correct order for halfedge elimination; 2026-06-11 18:29:11 +02:00
dependabot[bot] faed3e517c Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-11 18:29:11 +02:00
dependabot[bot] 0d021585cb Bump astral-sh/setup-uv from 3 to 7
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 3 to 7.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v3...v7)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-11 18:29:11 +02:00
dependabot[bot] a195056a25 Bump hendrikmuhs/ccache-action from 1.2.22 to 1.2.23
Bumps [hendrikmuhs/ccache-action](https://github.com/hendrikmuhs/ccache-action) from 1.2.22 to 1.2.23.
- [Release notes](https://github.com/hendrikmuhs/ccache-action/releases)
- [Commits](https://github.com/hendrikmuhs/ccache-action/compare/v1.2.22...v1.2.23)

---
updated-dependencies:
- dependency-name: hendrikmuhs/ccache-action
  dependency-version: 1.2.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-11 18:29:11 +02:00
dependabot[bot] cc77fe2007 Bump ruff from 0.15.10 to 0.15.12
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.10 to 0.15.12.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.10...0.15.12)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-11 18:29:11 +02:00
dependabot[bot] 4a532c7de7 Bump ty from 0.0.29 to 0.0.32
Bumps [ty](https://github.com/astral-sh/ty) from 0.0.29 to 0.0.32.
- [Release notes](https://github.com/astral-sh/ty/releases)
- [Changelog](https://github.com/astral-sh/ty/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ty/compare/0.0.29...0.0.32)

---
updated-dependencies:
- dependency-name: ty
  dependency-version: 0.0.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-11 18:29:11 +02:00
Ryan Schultz 25c2464cd5 Fix #7885: LAYER3 crash on IfcCompositeProfileDef
The x-angle transformation for LAYER3 slabs assumed SweptArea
is always IfcArbitraryClosedProfileDef (which has OuterCurve),
but composite profiles use IfcCompositeProfileDef instead.
Apply the coord scaling to each sub-profile individually.

Generated with the assistance of an AI coding tool.
2026-06-11 18:29:10 +02:00
Ryan Schultz aba5889dec Fix #7927: Fix SECTION annotation for MODEL_VIEW drawings
generate_section_reference_points had no handler for
MODEL_VIEW target view, causing it to silently return
None. Add MODEL_VIEW branch that clips the section line
to XY camera bounds while preserving the Z coordinate
for correct 3D placement.

Generated with the assistance of an AI coding tool.
2026-06-11 18:29:10 +02:00
Ryan Schultz 68ecb97203 Fix #8024 - Fix TypeError when CardinalPoint is None
Guard the int() cast on CardinalPoint in
BIM_OT_edit_assigned_material so a None value (no cardinal
point set) no longer raises a TypeError.

Generated with the assistance of an AI coding tool.
2026-06-11 18:29:10 +02:00
E Shattow 991b41ac52 docs: project_overview: project_info blender tip to change display units after project creation
Link to Blender Manual for tip to change display units
2026-06-11 18:29:10 +02:00
Thomas Krijnen 66328d7fd1 Simple SPF submodule update 2026-06-11 18:28:36 +02:00
falken10vdl f2ddda8f83 Fix IfcSurfaceStyleRendering colour reset on save 2026-06-11 18:27:27 +02:00
Thomas Krijnen 9d1cd6adf6 Update build_pyodide.sh to source emsdk_env.sh conditionally
Add conditional sourcing for emsdk_env.sh
2026-06-11 18:27:26 +02:00
Thomas Krijnen a5cd8d025d arrange_polygons: Revert to unsimplified when big IoU difference; threshold on max snap distance; write most deviating input-output pair to debug output 2026-06-11 18:26:42 +02:00
Richard Brice 24a40625d1 Fixes bug in addRelatedObject<> for IfcRelReferencedInSpatialStructure 2026-06-11 18:26:42 +02:00
Bruno Postle 60cb034df7 Add license for OpenGost font shipped with Bonsai
Extracted from the font file like so:
python3 -c "
  from fontTools.ttLib import TTFont
  tt = TTFont('src/bonsai/bonsai/bim/data/fonts/OpenGost Type B TT.ttf')
  for record in tt['name'].names:
      if record.nameID == 13:
          print(record.toUnicode())
  "
2026-06-11 18:25:47 +02:00
Massimo Fabbro 00f6241417 See #6853. Minor fix for IfcDoor with IFC4x3 quantity calculation with blender engine 2026-06-11 18:25:47 +02:00
Massimo Fabbro 1512947c83 See #7716. Remove_cost_item also delete the assignment
Previously remove_cost_item leaved orphaned relation now it should be fixed
2026-06-11 18:25:46 +02:00
Massimo Fabbro 0ca9fd2773 See #7716. Fix util get_cost_item_for_product
Before there was an error if there weren't assignments now it should be fixed. Add also tests.
2026-06-11 18:25:46 +02:00
Massimo Fabbro 11d6508476 Add tests for cost tool 2026-06-11 18:25:46 +02:00
Massimo Fabbro efbe9a543f fix infinite recursion error
previously there was an almost silent error because the update function was called every time. Now it should be fixed.
2026-06-11 18:25:46 +02:00
Thomas Krijnen fc1af4ed93 ifcchat: update ifopsh to latest wasm wheel 2026-06-11 18:25:46 +02:00
Andrej730 3e28bf00f3 maintenance: rename main.yml to publish-websites.yml in docs 2026-06-11 18:25:46 +02:00
Andrej730 9003750ea1 build_rocky: use uv to acquire more recent version of Python 2026-06-11 18:25:46 +02:00
Andrej730 0cc4255f23 Makefiles - refer to python in more generic way 2026-06-11 18:21:12 +02:00
Andrej730 5388d34593 maintenance: add publish-bonsai-releases.py to Blender Python version update checklist 2026-06-11 18:21:12 +02:00
Andrej730 07af7ea7a3 maintenance: add documentation about multiple Blender Python versions 2026-06-11 18:21:12 +02:00
Andrej730 e99f85f9a6 maintenance: add corrective release documentation 2026-06-11 18:21:12 +02:00
Andrej730 1ccdcd75b7 black . 2026-06-11 18:21:12 +02:00
Andrej730 76561c040e Add workflow to publish bonsai releases to Blender Extensions 2026-06-11 18:21:12 +02:00
dependabot[bot] 613fc6ffb1 Bump ruff from 0.15.9 to 0.15.10
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.9 to 0.15.10.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.9...0.15.10)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-11 18:21:12 +02:00
Thomas Krijnen 4c13e2424c Configurable pointer type; std::from_chars(); aggregate inverses in vector; skip parse_context 2026-06-11 15:51:40 +02:00
Gorgious56 0c993d3292 Guard HasShapeAspects access on IFC2X3 representation iteration
IFC2X3 representations have no HasShapeAspects inverse; opening the
Geometry & Materials subpanel on an IFC2X3 object raised AttributeError
and left the items list empty. Wrap the access with a getattr default
so pre-IFC4 schemas return an empty iterable, and pin the contract with
an AST forward-compat guard that scans bim/, tool/, and core/ for any
future direct .HasShapeAspects access.

Closes #8157

Generated with the assistance of an AI coding tool.
2026-06-11 09:28:27 +02:00
Richard Brice 32a601d057 fixes build problem from commit a7738eeb 2026-06-10 13:31:32 -07:00
Gorgious56 d1d1e1d4a2 Add railing parametric edit + schematic preview
Port gizmos-8088's railing gizmo block to v0.8.0:

- _RailingEditMixin (PathPreservingEditMixin specialisation) +
  EnableEditingRailing / CancelEditingRailing / FinishEditingRailing
  edit triad
- CycleRailingType (2-value type cycler) + ToggleRailingUseManualSupports
  one-shot + EditRailingTerminalType
- FlipRailingPathOrder + EnableEditingRailingPath /
  CancelEditingRailingPath / FinishEditingRailingPath path-edit
  operators (mutually exclusive with the schematic frame)
- GizmoRailingSchematic (BaseSchematicGizmoGroup specialisation) —
  axonometric schematic frame with per-attribute dimension gizmos
  for FRAMELESS_PANEL + WALL_MOUNTED_HANDRAIL railing types;
  hover-on-attr highlights the schematic edges tagged with the
  matching feature

Tests: test_railing_lifecycle.py (280 LOC) +
test_railing_schematic.py (272 LOC).

Drops the per-feature GizmoPreferences{Door,Window,Stair,Wall,Roof,
Railing} PropertyGroups that the source commit added to bim/ui.py
— that finer-grained per-attribute toggle model was deliberately
collapsed to flat per-feature bools in the PR5b prefs sweep, and
GizmoRailingSchematic gates on the flat ``prefs.gizmos.railing``
bool via ``gizmo_pref_name`` so no functionality is lost.

Generated with the assistance of an AI coding tool.
2026-06-10 20:40:33 +02:00
Thomas Krijnen a7738eeb64 Pass around non-static logger instances and programmatic access to messages in-memory 2026-06-10 18:40:17 +02:00
Thomas Krijnen a751fb956d Introduce unique error codes 2026-06-10 18:40:17 +02:00
Gorgious56 251157f8d4 Fix np_frombuffer_legacy length-vs-dtype check
The check `len(bytedata) == n * 2` was wrong: float64 is 8 bytes per
element, not 2. Legacy float64 checksums fell through to the float32
reader and produced a (2n,)-shaped array, breaking is_moved() and
is_camera_moved() with `ValueError: operands could not be broadcast`
on .blend files saved by Blender <5.0.

Adds a parametrized regression test covering both n=3 (translation)
and n=9 (rotation) for both dtypes.

Generated with the assistance of an AI coding tool.
2026-06-10 17:37:41 +02:00
Gorgious56 a213a9b848 Merge pull request #8155 from Gorgious56/bonsai/mep-edit-gizmos
Add MEP segment + bend edit gizmos
2026-06-10 17:35:33 +02:00
Gorgious56 b76bc1c1f8 Read wall extent from bbox in cursor gizmo layout
GizmoWallEdition.position_gizmos used props.anchor_x / props.length
for the in-range check (split icon visibility) and perpendicular
gizmo placement. Those props mirror IFC and are re-primed by
_maybe_resync_wall_props_from_ifc — any operator path that skips
the re-sync leaves the perpendicular gizmo clamped to the previous
wall extent, so the icon parks at the old wall end instead of the
cursor's orthogonal projection. Visible after a wall mutation as
the perpendicular icon landing way off the cursor in top-down view.

Switch to the mesh bbox along local X. recreate_wall rebuilds the
mesh to match the current IFC body on every wall mutation, so
bound_box is authoritative without an explicit props sync.

Generated with the assistance of an AI coding tool.
2026-06-10 13:17:07 +02:00
Gorgious56 4d24cef0c9 Hide MEP gizmos on non-parametric elements
MEP elements imported as tessellation / brep (no IfcExtrudedAreaSolid
or IfcSweptDiskSolid in their body representation) can't be
parametrically edited — the gizmos offer affordances the geometry
kernel has no path to honour. tool.System.has_parametric_body
inspects the Model/Body/MODEL_VIEW representation and returns True
only when at least one item resolves to one of the two
profile-sweep primitives.

The gate is wired into:
- GizmoMEPActions.is_eligible_object (the action icon group)
- _active_is_flow_segment / _active_is_bend_fitting visibility
  predicates the icon row consults per-icon
- GizmoPipeSegmentEdition / GizmoDuctSegmentEdition is_element_type

tool.Parametric.is_pipe_segment / is_duct_segment stay IFC-class-only
so their truth-table contract test keeps reading a single concern.

Generated with the assistance of an AI coding tool.
2026-06-10 13:00:01 +02:00
Gorgious56 6c9cfccc43 Move _is_multiple_of_pi to tool.Cad
Pure-math parallelism check (value ≡ 0 mod π within VTX_PRECISION)
that lived as a module-private helper in mep.py belongs next to
tool.Cad.is_x — same comparator family, no MEP-specific knowledge.
Other features with rotation-difference checks (wall fillet, roof
slope, railing terminus) now have a sanctioned spelling.

Generated with the assistance of an AI coding tool.
2026-06-10 12:35:21 +02:00
Gorgious56 0a0a5b9f04 Add MEP cache + smoke + cancel-ops forward-compat tests
Four standalone test files pinning contracts the production code
already honours:

- test_mep_actions_cache.py: GizmoMEPActions visibility-predicate
  cache evicts on selection or generation change.
- test_mep_bend_preview_cache.py: bend decorator polyline cache
  re-uses within a generation and rebuilds on generation bump.
- test_mep_distribution_fit_smoke.py: bim.fit_flow_segments
  round-trips a 3-segment polyline without raising.
- test_preview_cancel_ops_forward_compat.py: AST scan ensures every
  preview Enable* operator has a paired Cancel* operator with the
  matching prop reset.

Generated with the assistance of an AI coding tool.
2026-06-10 12:27:23 +02:00
Gorgious56 3a0abbab95 DRY transform-modal draw gate + polyline helper
Two small refactors:

- apply_transform_modal_draw_gate(group, context) replaces the
  three-line _is_transform_modal_active + _hide_all_non_modal_gizmos
  pair that BillboardingGizmoGroupMixin, BaseParametricGizmoGroup
  and BaseSchematicGizmoGroup all repeat in draw_prepare.
- decorator.py renames _stroke_lines_alpha to a public-scope
  draw_polyline_segments and drops the no-longer-private companion
  docstring reference; the function is now usable by sibling
  decorators that draw polyline overlays.

Plus a few one-liner tweaks in tool/model.py and opening.py
following the helper rename.

Generated with the assistance of an AI coding tool.
2026-06-10 12:26:48 +02:00
Gorgious56 0d703039a6 Cache array-child + wall topology by IFC generation
Two hot paths the gizmo polls fire every viewport event memoise
their result against tool.Parametric.get_geom_generation():

- tool.Blender.Modifier.any_selected_array_child caches the
  per-selection scan against the selection identity-set + the
  IFC generation token so a stable selection during a drag
  doesn't re-walk every selected object's BBIM_Array pset every
  frame.
- bim/module/model/wall.py grows a pair-predicate + connection
  cache that the wall topology gizmos hit; both keyed on
  (pair_uids, predicate_kind, generation) so a wall split or
  axis edit invalidates correctly via the generation bump.

Behavioural contract is unchanged — stale entries are evicted
on generation bump; cache miss returns the same value the
un-cached path returned.

Generated with the assistance of an AI coding tool.
2026-06-10 12:25:42 +02:00
Gorgious56 f33df52c1b Centralise model test fixtures via conftest
bim/module/model/conftest.py exposes the autouse _require_real_bpy
skip-guard, four make_* factories (obj / element / context /
ifc_file), and a patched_tool context-manager factory that wires
the half-dozen tool.* boundary patches every gizmo + decorator
test was repeating.

Existing test files in the directory drop their local copies of
_require_real_bpy and adopt the patched_tool / make_* fixtures
where the call site simplifies — test_mep_port_operators.py is
the biggest beneficiary (−89 LOC).

No production behaviour change.

Generated with the assistance of an AI coding tool.
2026-06-10 12:24:44 +02:00
Gorgious56 15a6375ea3 Extract MEP bend preview + refine port operators
Three concerns bundled by file boundary (all in mep.py):

- Extract bend preview operators + GizmoBendPreview into a focused
  mep_bend_preview.py module; preview_base.py grows the shared helper
  set both bend and other previews now consume; classes tuple in
  model/__init__.py updated to register the new module.
- Surface ERROR reports on five silent CANCELLED returns in
  MEPUnjoinAtPort / MEPRemoveTerminalFitting / MEPUnjoinPair so a
  degenerate IFC file ("fitting has no Blender object", "connected
  port leads nowhere") shows up in the popup instead of looking like
  a no-op.
- DRY: _resolve_active_mep_segment + _require_port_state factor the
  segment-id-or-active-object resolve + port-state guard out of every
  port operator's prologue; _wire_anchored_icon_targets pulls the
  GizmoMEPActions setup() body into an exercise-without-MRO helper so
  the wiring-contract tests can hit it without instantiating the
  GizmoGroup.

Drops the now-unused preview_base import that the extraction left
behind.

Generated with the assistance of an AI coding tool.
2026-06-10 12:24:05 +02:00
Gorgious56 82465a64a5 Brighten and dash opening occlusion outline
The opening preview's outline used a single-batch two-pass scheme that
dimmed the occluded back pass via alpha=0.25. The visible front pass also
inherited the source decorator color's modest alpha, so the outline read
as subtle on both sides.

Replace with a CAD hidden-line convention: solid full-alpha front pass on
the visible side, world-space dashed back pass on the occluded side. Both
passes use POLYLINE_UNIFORM_COLOR so depth and line-weight paths match.
The dashed batch is built once per object epoch by a new pure helper
tool.Blender.build_dashed_line_segments (pre-segments edges into world-
space dash chunks), then cached via the existing batch-cache mechanism
under "<uid>_dashed".

The solid front pass is rendered at a slightly wider line width than the
dashed back pass so its halo overpowers Blender's WIRE-display overlay
bias at outline pixels — without the asymmetry the wire's anti-z-fight
forward bias makes the LESS_EQUAL comparison narrowly fail and the
dashed pass wins on visible edges too.

Generated with the assistance of an AI coding tool.
2026-06-09 22:49:00 +02:00
Gorgious56 6bde619fe6 Migrate MEPConnectElements args from object names to IFC GUIDs
MEPConnectElements took obj1_name/obj2_name (Blender object names),
which break when objects are renamed or replicated by array
duplication. Switch to obj1_guid/obj2_guid resolved via
ifc_file.by_guid, with by_guid RuntimeError surfaced as an operator
error rather than a stack trace. DrawPolylineProfile (the sole
in-tree caller) updates to pass GlobalIds.

Generated with the assistance of an AI coding tool.
2026-06-09 22:47:10 +02:00
Gorgious56 ba5321fdfa Add MEP bend tessellation helper tests
Pins the geometry contracts the hand-meshed bend body relies on
while IfcSweptDiskSolid round-trip is broken upstream (#8106):

- profile cross-section sampling: circle returns 16 evenly-spaced
  points starting at (radius, 0); rectangle returns the four
  canonical corners; anything else returns None so the rep swap
  is skipped rather than meshed against the wrong section
- parallel-transport framing keeps the cross-section continuous
  around L-shaped corners — pinned via start / end ring planes
- initial_basis override seeds the first ring with the source
  segment's local +X / +Y axes, fixing the asymmetric-rectangle
  twist the world-Z seed produces

Generated with the assistance of an AI coding tool.
2026-06-09 22:18:22 +02:00
Gorgious56 49ddc97918 Add MEP port operator dispatch tests
Pins which IFC mutation each port operator commits and which
inputs each refuses with CANCELLED:
- MEPUnjoinAtPort removes the fitting + reconnects the two free
  ports; refuses if the named port is free or terminal
- MEPRemoveTerminalFitting deletes the terminal element + leaves
  the segment's port free; refuses on bridged fittings
- SelectMEPPathMembers walks IfcRelConnectsPorts in both
  directions from the active segment and selects every fitting /
  segment reachable through the port graph

Boundary mocks for tool.Ifc, tool.System and MEPGenerator stand
in for the IFC fixture; tests assert against the recorded
ifcopenshell.api.* calls.

Generated with the assistance of an AI coding tool.
2026-06-09 21:46:34 +02:00
Gorgious56 39bcd9db63 Add GizmoMEPActions wiring contract tests
Pins two regressions the live MEP gizmo group can hit:
- per-icon setup() must write `position` (and `mode` on open-lock
  icons) onto every target_set_operator result; the test stands in
  for the AttributeError on bim.mep_add_obstruction that surfaced
  when a field was dropped from the operator declaration
- each visibility_condition lambda must stay total against None /
  non-IFC inputs, since a single raising predicate silently disables
  every sibling icon in the group

Generated with the assistance of an AI coding tool.
2026-06-09 21:45:34 +02:00
Thomas Krijnen ab11ac5338 Catch decomposition errors #8149 2026-06-09 21:35:40 +02:00
Gorgious56 9346f45bba Fix decorator face-tri overlay artifacts
ProfileDecorator.draw_faces (used by the roof path-edit overlay) and
SystemDecorator.draw_faces called bmesh.ops.triangulate on the live
bmesh — both mutated the input and produced ear-clip fans that rendered
as visible streaks across n-gon roof faces at alpha 0.1. The opening
DecorationsHandler edit-mode branch had a separate bug: it computed
triangles from obj.data.calc_loop_triangles() while iterating the
edit-mode bmesh, so any topology added mid-edit desynced the indices.

Centralise the correct draw path on tool.Blender.draw_bmesh_face_tris
(wraps bm.calc_loop_triangles, non-mutating, beauty triangulator) and
route all three call-sites through it. A forward-compat AST guard walks
every *Decorator / DecorationsHandler class under bim/module/ and pins
the no-bmesh.ops.triangulate rule against future regressions.

Generated with the assistance of an AI coding tool.
2026-06-09 20:16:46 +02:00
Gorgious56 b22687891b Warn on shared-rep parametric edits
A user clicking the pen icon on a typed-product occurrence whose body
representation is mapped from its type would silently mutate every
sibling occurrence's geometry. Add a confirmation dialog at the pen-icon
dispatcher (the single chokepoint every feature routes through) showing
the sibling count, with a session-scoped suppress checkbox.

The check is read-only: tool.Model.get_sibling_occurrence_count wraps
tool.Geometry.get_elements_by_representation against the resolved body
rep and subtracts self + type. A forward-compat AST guard pins the
dispatcher monopoly so any future feature that binds pen_gizmo directly
to a feature-specific enable op fails the test before merge.

Generated with the assistance of an AI coding tool.
2026-06-09 17:32:44 +02:00
Gorgious56 784f0b1fe2 Add bend re-edit gizmo
Once a bend was created, the only way to retune start_length /
end_length / radius was to delete and recreate from scratch.
EnableBendPreviewFromBend re-opens the preview on an existing
parametric bend: it walks the bend's ports to resolve the two
connected segments, reads start / end length and radius from the
bend type's BBIM_Fitting pset, and sets editing_bend_id on the
preview props. MEPAddBend then deletes the old bend + its port
connections (single undo step) before the recreate path runs, so
finish replaces the bend in place and cancel discards the edit
without touching the original.

GizmoMEPActions surfaces a pen icon on single bend-fitting
selections via the new _active_is_bend_fitting predicate; the icon
dispatches the new operator. Mirror of the wall fillet re-edit
flow (EnableWallFilletPreviewFromCorner + editing_corner_id in
CreateWallFillet).

Test coverage: registration probe for the new operator, an attached
editing_bend_id field probe on the preview umbrella, and a
parametrized truth-table for the _is_bend_fitting predicate
(IfcFlowFitting with BEND PredefinedType, with other PredefinedType,
with no type, IfcFlowSegment, IfcWall, None).

Generated with the assistance of an AI coding tool.
2026-06-09 17:18:09 +02:00
Gorgious56 e0ceda6856 Hide wall topology gizmos on array children
Wall topology mutations (merge / join / extend-to-wall / unjoin /
fillet) applied to a Bonsai array child are silently overwritten by
the next ``regenerate_array``; merge also orphans a GUID listed in
the parent's ``BBIM_Array.Data``. Add a central
``tool.Blender.Modifier.any_selected_is_array_child`` predicate and
gate the five wall topology gizmo groups plus the six bound operators
behind it. Operator gating is defence in depth against keymap / F3
invocation paths that bypass the gizmo.

The base ``_wall_gizmo_poll_gate`` keeps its loose two-check shape
(viewport gizmos + no preview). A new
``_wall_topology_gizmo_poll_gate`` wraps it with the array-child
filter and is what the topology gizmos use. Host-opening gizmos
deliberately stay on the loose gate: openings authored on a child
are preserved through ``regenerate_array`` and track with the
replicated instance.

A forward-compat AST guard walks wall.py for ``GizmoGroup`` subclasses
and asserts each routes its poll through the tighter gate or the
central predicate, with an allow-list for the parametric-edit and
preview-owner exceptions. New wall topology gizmos inherit the
contract by construction.

Generated with the assistance of an AI coding tool.
2026-06-09 17:16:12 +02:00
Gorgious56 17951427fe Add readonly door swing arc preview
Selecting a Bonsai-parametric IfcDoor now shows the swing arc(s)
without entering edit mode. A new viewport decorator polls on the
active object, reads the door's BBIM_Door pset, and draws the same
arcs the parametric door swing gizmo would draw — matching the
hinge / panel-width / x-mirror contract minus the is_editing gate.

A forward-compat test walks every door operation type and cross-
checks the readonly decorator's arc selection against the gizmo's
swing-arc config table, so future enum additions fail in both
surfaces simultaneously.

Also disables the inherited 8-pass dark halo on GizmoArc: an open
curve has no enclosed silhouette, so the offset passes read as
ghost arcs rather than a uniform outline. The arc's own cross-
section thickness keeps it legible without the halo.

Generated with the assistance of an AI coding tool.
2026-06-09 16:13:59 +02:00
Gorgious56 734f4df84e Add MEP bend preview + bend tessellation fallback
The MEP bend feature's IfcSweptDiskSolid representation produces
geometrically correct output but fails to round-trip through the
OpenCascade geometry kernel (upstream issue #8106) — the body is
dropped on the next file load. Until upstream is fixed, MEPAddBend
captures the bend centerline in world space before the segments are
extended (otherwise the post-extension axes no longer reach the
original intersection and arc reconstruction is wrong), then after
the fitting is placed it hand-meshes the bend body and swaps the
type's swept-disk representation for an IfcTessellatedFaceSet via
tool.Geometry.export_mesh_to_tessellation + tool.Model.
replace_object_ifc_representation.

The centerline includes the straight start_length / end_length legs
in addition to the arc so the bend covers the full segment-to-
segment span. Sweep uses parallel-transport framing — each ring's
(right, up) basis is rotated by the minimum rotation that maps the
previous tangent to the current one, eliminating the twist a fixed
world-axis reference produces when the tangent crosses the
reference. Cross-section orientation seeds from the source segment's
matrix_world local +X / +Y so asymmetric IfcRectangleProfileDef
ducts land with XDim / YDim on the same axes the segment expects;
parallel transport then preserves that alignment around the arc.
Centerline radius is radius + profile_dim[lateral_axis] to match
MEPAddBend's ref_point_radius — without this offset, the bend legs
fall short of the extended segments by profile_dim * tan(angle/2).
Face winding is left to the caller to correct via
bmesh.ops.recalc_face_normals on the closed bend tube.

Two FIXME(#8106) markers (capture site + helper call site) so both
can be dropped once upstream lands a swept-disk round-trip fix.

Generated with the assistance of an AI coding tool.
2026-06-09 16:12:35 +02:00
Ryan Schultz bfa2d789f1 Error on tessellation request in IFC2X3
IfcTriangulatedFaceSet/IfcPolygonalFaceSet were introduced in
Fix #7992: IFC4 and do not exist in IFC2X3. Previously, requesting an
IfcTessellatedFaceSet representation in an IFC2X3 file silently
fell back to a faceted brep after unassigning material sets.
Add a guard in the update_representation operator (user-facing
error) and in the add_representation API (ValueError) so the
unsupported request is caught instead of failing silently.

Generated with the assistance of an AI coding tool.
2026-06-09 07:30:59 -05:00
Gorgious56 192bf00d31 Add cursor-bound perpendicular wall gizmo
GizmoWallEdition gains a fourth cursor-anchored icon that
spawns a perpendicular branch wall from the cursor's
orthogonal projection on the source wall axis. Click forms
a T-junction; shift+click forms an L-corner with the source
wall trimmed at the projection, keeping its longer portion.

The branch inherits the source's spatial container and
centerline baseline so its authored axis matches the source's
alignment rather than the type's default.

Also includes a floor-plane preview quad for the new gizmo,
a floor-Z cross line on the split preview for top-down
visibility, a small bump to QUAD_ALPHA for clearer preview
fills, and a stacking-offset helper that centralises the
cursor-row screen-up step across three call sites.

Generated with the assistance of an AI coding tool.
2026-06-09 13:54:39 +02:00
Gorgious56 ad672d0edb Add GizmoMEPActions + bend precondition + obstruction modes
The MEP one-shot operators (join, unjoin variants, terminal removal,
path-select, obstruction add/remove) had no viewport surface. This
commit adds GizmoMEPActions — the icon-action gizmo group that
surfaces them as billboarded icons around selected MEP elements.
Three anchor regions: a horizontal row above the bbox top
(selection-cardinality icons), per-port endpoints for the three-state
lock / unjoin icons (open lock for PORT_FREE, closed for
PORT_TERMINAL, unjoin for PORT_JOINED — resolved per-frame from
port_connection_state), and the predicted join location
(compute_mep_join_location, shared with the bend preview) for the
join / unjoin_pair pair. Unjoin icons render at full
DEFAULT_BILLBOARD_SCALE with warning-red hover; endpoint lock icons
shrink so the lock row stays subordinate to the row icons. The
group hides itself entirely while a bend preview is active.

MEPAddObstruction grew a position enum (CURSOR / START / END) and a
mode enum (ADD / REMOVE / TOGGLE) so the gizmo can target a specific
port without touching the cursor and dispatch ADD or REMOVE based on
the click target — the lock_open icons drive ADD with position
pinned, the lock_closed icons drive bim.mep_remove_terminal_fitting.
Without the new fields the gizmo wiring (op_props.position = ...)
crashed at setup() with AttributeError on the obstruction operator.

validate_bend_preconditions extracts the type-match and profile-kind
checks MEPAddBend enforces so EnableBendPreview surfaces the
rejection immediately — the user no longer tunes a preview only to
learn at commit time that the segments use an unsupported profile
(e.g. IfcArbitraryClosedProfileDef).

Generated with the assistance of an AI coding tool.
2026-06-09 12:44:19 +02:00
Gorgious56 0df1f0cf49 Add MEP unjoin / terminal-remove / path-select operators
Four discrete one-shot operators driven by the MEP segment's port
state. mep_unjoin_at_port deletes the IfcFlowFitting bridging a
segment's named port to a second element when the port is in the
JOINED state. mep_remove_terminal_fitting deletes the terminal
fitting at a port (closed-lock state) and dispatches by fitting
type — OBSTRUCTION fittings go through MEPGenerator.remove_obstruction
so the segment absorbs the freed length, other terminal fittings go
through the standard delete path. mep_unjoin_pair finds the single
fitting bridging two selected MEP segments and deletes it.
select_mep_path_members walks the connected MEP network from the
active element via IfcRelConnectsPorts and replaces the selection
with every reachable member. Foundation for the MEP Actions gizmo
group which surfaces these operators as icon affordances around
selected segments.

Generated with the assistance of an AI coding tool.
2026-06-09 12:16:15 +02:00
Gorgious56 d7dd8ecf57 Align extend gizmo arrow with segment axis
The extend icon used a pure screen-space billboard that always
pointed +X across the screen — the arrow ran horizontally
regardless of the pipe / duct's orientation. The new
billboarded_along_axis helper rotates the gizmo about the camera-
forward axis so its local +X aligns with the segment's local +Z
projected onto the screen, keeping the icon camera-facing but
visually following the extrusion direction. The flip-mirror branch
now reads from cursor-vs-current-end along the segment axis (not
screen-X), so the arrow points away from the current endpoint
regardless of viewport orientation. The split icon stacks
perpendicular to the rotated extend arrow in screen space so the
two don't overlap.

The decorator's green preview line no longer clamps the cursor
projection to min_projected_length — it follows the raw projection
so the line stays visible when the cursor crosses behind the
segment origin (the user still sees where they're pointing even
though the operator floors the actual commit).

Generated with the assistance of an AI coding tool.
2026-06-09 11:51:37 +02:00
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 93c6350e0f Merge pull request #8148 from Gorgious56/bonsai/parametric-framework-features-pt2
Add parametric edit framework features (pt2): gizmo + UX polish
2026-06-07 02:09:00 +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
Bruno Postle 674ed36e41 Fix HDF5 config-mode detection to use shared library when static is absent
When HDF5 is found via its CMake config file, the code previously hardcoded
the hdf5_cpp-static target. On distributions that ship only shared HDF5
(e.g. Fedora rawhide where the config file was added in a newer package),
this caused a link failure. Now checks for hdf5_cpp-static, hdf5_cpp-shared,
and hdf5::hdf5_cpp-shared in order, falling back to module-mode discovery.
2026-06-05 08:53:10 +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
Dion Moult 1a17ba9e6d ifcviewer: de-Qt QColor/QPoint/QSet/QElapsedTimer in ViewportWindow
Last round of straight-swap Qt value types in ViewportWindow + its
overlay co-pilot.

  setBackgroundColor(const QColor&)   → (float r, float g, float b, float a)
  QColor   background_color_          → Eigen::Vector4f (linear, 0..1)
  QPoint   {nav_,box_select_,fps_,    } → Eigen::Vector2i
           {section_drag_start_mouse_}
  QSet<int> fps_keys_held_            → std::unordered_set<int>
  QElapsedTimer fps_last_tick_,       → Stopwatch (new header in
               fly_render_clock_,        IfcViewerCore — std::chrono-
               render_thread_local_      backed, exposes the existing
               timers in render()        QElapsedTimer .start/.restart/
                                         .elapsed/.nsecsElapsed surface)

Also propagates the QPoint → Eigen::Vector2i change through
OverlayRenderer::encodeMarquee since the marquee corner coords flow
through that interface.

API-level helpers:
  toV2i(QPoint)        — small inline in ViewportWindow.cpp, isolates
                         the QMouseEvent→Vector2i conversion at the
                         five mouse-event handlers
  Stopwatch.h          — new file, IfcViewerCore. Same call shape as
                         QElapsedTimer; backed by std::chrono::steady_clock.

QSet method swaps:
  .isEmpty() → .empty()
  .contains(k) → .count(k)   (C++17, no std contains() until C++20)
  .remove(k)   → .erase(k)

Eigen::Vector2i doesn't have .manhattanLength(); the box-select drag
threshold uses std::abs(diff.x()) + std::abs(diff.y()) inline.

Bonsai side: View.cpp's setBackgroundColor wrapper now decomposes the
QColor into floats at the call site (kept locally so the bonsai UI
keeps its QColor-driven theming).

Closes #81 + the QElapsedTimer half of #83. QTimer
(pivot_indicator_hide_timer_) still uses Qt — it needs the host's
scheduleOnce mechanism that lands with #85.

Builds: desktop / bonsai / web all green. Tests 100/100.
2026-06-05 09:26:53 +10:00
Dion Moult b62e14a06a ifcviewer: de-Qt ViewportWindow public API (QString → std::string)
Take QString out of ViewportWindow's outward-facing surface so it can
eventually move into a Qt-free ViewportCore:

  void     queueLoadSidecar(const QString&)    →  (const std::string&)
  uint32_t loadSidecar(const QString&)         →  (const std::string&)
  QString  cameraString() const                →  std::string …
  void     captureNextFrameToPng(const QString&, bool)
                                               →  (const std::string&, bool)
  void     setHudText(const QString&)          →  (const std::string&)

Internal members also moved off QString:
  std::deque<QString> pending_sidecars_     →  std::deque<std::string>
  QString             pending_screenshot_path_ →  std::string

Implementation strategy: convert at the boundary where ViewportWindow
still leans on Qt internals — `loadSidecar` bridges to QString once
for QFile/QDir/QFileInfo path handling; the screenshot save path
constructs a QString locally for QImage::save; the OverlayRenderer's
HUD setter still takes QString so setHudText converts before calling
through. Each of those bridges goes away when ViewportCore lands and
OverlayRenderer / SceneLoader / SidecarBuilder get their own de-Qt
sweeps. cameraString now produces its CSV via snprintf — no QString
ever instantiated.

Bonsai-side updates (compile-only):
  ifcviewer-minimal/main.cpp — queueLoadSidecar / captureNextFrameToPng
                              callers add .toStdString() on the QString
                              parser result
  modules/viewport/View.cpp  — setHudText callers add .toStdString() to
                              their `QString::arg(...)` formatter chains;
                              two `QString()` empty sentinels become
                              `std::string()`
  Measurement.cpp            — same pattern, two setHudText sites
  ifcviewer/LengthMeasurement.cpp — same, three sites

The cameraString string-streaming fix-up in ViewportWindow.cpp drops
the temporary .toUtf8().constData() bridge from #82 — Log::Stream's
std::string overload now handles it directly.

Builds: desktop / bonsai / web all green. Tests 100/100. Closes #80.
2026-06-05 09:14:19 +10:00
Dion Moult 6dd3558db9 ifcviewer: replace qInfo/qWarning with a Qt-free logger seam
Add Log.h (in IfcViewerCore) — a tiny stream-style logger that backs
fprintf(stderr,...), with overloads for the common primitives + char
strings. Mimics qInfo()/qWarning()'s syntax surface enough that
mass-replacing qInfo()→Log::info() and qWarning()→Log::warn() keeps
existing call sites parsing unchanged; .noquote() / .nospace() exist
as compat no-ops so chained qInfo().noquote()<<x<<y patterns survive.

QString streaming is a transitional concern — the QString → std::string
sweep (#80) hasn't landed yet, so ViewportWindow and friends still
construct QStrings for log payloads. LogQt.h (in IfcViewer, not Core)
adds the QString / QStringView operator<< overloads so those streaming
sites work without source changes during the in-flight Qt removal.
When #80 retires QString, LogQt.h drops out.

ViewportWindow.cpp: 132 qInfo/qWarning callsites converted. The two
printf-style qInfo("fmt %s", ...) callsites get fprintf with explicit
[info]/[warn] prefixes to keep the output discoverable.

Also de-Qt'd:
  AreaMeasurement.cpp  — 1 qInfo("fmt", …) → fprintf
  SceneLoader.cpp      — 4 qDebug + 1 qWarning printf-style → fprintf
  GeometryStreamer.cpp — 2 qDebug printf-style → fprintf
  ifcviewer-minimal/main.cpp — 2 qWarning << → Log::warn

Drops <QDebug> from each. Closes #82.

Builds: desktop / bonsai / web all green. Tests 100/100 pass.
2026-06-05 08:58:45 +10:00
Dion Moult 77cf535b45 ifcviewer: de-Qt math types (Eigen everywhere)
Replace Qt math wrappers with Eigen across ViewportWindow, OverlayRenderer,
Federation, and the bonsai-side viewport modules. Eigen was already the
canonical type for the actually-important matrix work (InstanceCompose,
ModelGpuData, federation matrices); QVector3D/QVector4D/QMatrix4x4 were
leftover from when Qt was the path of least resistance. They offered
nothing over Eigen for our use case beyond a few graphics helpers
(lookAt / perspective / ortho) which were 30 lines to write.

Substitutions:
  QMatrix4x4 → Eigen::Matrix4f
  QVector2D  → Eigen::Vector2f
  QVector3D  → Eigen::Vector3f
  QVector4D  → Eigen::Vector4f

API rewrites:
  .lengthSquared()         → .squaredNorm()
  .length()                → .norm()
  .isNull()                → .isZero()
  .setToIdentity()         → .setIdentity()
  .constData()             → .data()
  .toVector3D()            → .head<3>()
  .inverted(&ok)           → tryInvert4f(M, out)
  Q::dotProduct(a,b)       → a.dot(b)
  Q::crossProduct(a,b)     → a.cross(b)
  QMat4x4(... row-major)   → Eigen::Map<const Matrix4f>(col-major buf)
  QMat4x4().lookAt(...)    → lookAtRH(eye, target, up)
  QMat4x4().perspective(.) → perspectiveYFovGL(fovy, aspect, n, f)
  QMat4x4().ortho(...)     → orthoGL(l, r, b, t, n, f)

Default-init divergence handled explicitly (QMatrix4x4() = identity,
QVector3D() = zero; Eigen leaves both uninitialized). Public API
(CameraState, HomeView, ViewportWindow::computeObjectAabb, the
addSectionPlaneAtSurface / pickSurfaceAt / raycast signatures) follows
through to Eigen too; bonsai-side View.cpp and Commands.cpp updated to
match.

Camera helpers (lookAtRH, perspectiveYFovGL, orthoGL, tryInvert4f)
extracted to a new CameraMath.h so OverlayRenderer's gizmo MVP and
ViewportWindow's buildViewProj share the same definitions. Federation
drops its <QVector3D> include in favour of <Eigen/Dense> (already had
the latter for the georef matrices).

Builds: desktop IfcViewerMinimal ✓, BonsaiViewer ✓, web IfcViewerWeb ✓.
Tests: 100/100 pass. Closes #78 + #79; opens the door for #80-#83.
2026-06-05 08:33:22 +10:00
Thomas Krijnen 1f2b20fd86 Fix --convert-back-units on transformation object #8137 2026-06-04 22:15:35 +02:00
Thomas Krijnen 94fab271cd Check for empty result after BOPAlgo_MakerVolume and reset manifoldness state #8140 2026-06-04 21:45:27 +02:00
Thomas Krijnen 8583d0963f Make faceset duplicate loop detection respect inner/outer #8140 2026-06-04 21:45:27 +02:00
Thomas Krijnen 77a2284f8a Re-sew non-manifold operands; interior loop re-orientations affect edge identity #8140 2026-06-04 21:45:26 +02:00
Thomas Krijnen 4520a72152 Sane error messages for unsupported items in geometry libs #8106 2026-06-04 21:45:26 +02:00
Dion Moult c314dd3ca8 ifcviewer: scaffold ViewportHost + ViewportCore (Path A step 1)
Define the boundary the Path-A web-bring-up refactor will move things
across:

- ViewportHost.h is the embedder interface — surface creation,
  framebuffer geometry, frame scheduling, quit, and notification
  callbacks (onObjectPicked, onToolModeChanged, …). Desktop hosts
  forward notifications to Q_SIGNALS; the future web host pushes
  them to JS callbacks.

- ViewportCore.{h,cpp} is the platform-agnostic render-core target.
  Empty today — the body fills in across the #78-#86 sequence as
  each Qt subsystem (matrices, vectors, strings, timers, render
  path, input) gets de-Qt'd and moved over.

- ViewportWindow now multiply-inherits ViewportHost alongside QWindow
  and implements the host overrides as thin forwarders: createSurface
  returns the cached surface_, requestFrame -> requestUpdate, quit ->
  QCoreApplication::quit, onObjectPicked -> emit objectPicked.
  Renamed the DPR accessor `dpr()` (vs `devicePixelRatio`) to avoid
  the inherited-virtual clash with QWindow's qreal-returning version.

No method movement yet — this is purely the architectural scaffold so
subsequent commits have a destination.
2026-06-04 19:34:51 +10:00
Dion Moult e55a360aa2 web: scaffold IfcViewerWeb (Emscripten clear-color renderer)
First Emscripten target. main_web.cpp brings up a wgpu instance against
a <canvas id="viewer-canvas">, requests adapter+device asynchronously
via the standard webgpu.h callback chain, configures the surface, and
clears to the BonsaiViewer slate background on each RAF tick. No
sidecar load, no pipelines, no scene state yet — the goal is to end-
to-end verify the build + canvas + wgpu plumbing.

src/ifcviewer-web/ is a separate CMake root (not a subdir under the
desktop cmake/CMakeLists.txt) so the web build doesn't have to opt out
of Qt / OpenCASCADE / IfcGeom find_packages it can't satisfy. It adds
src/ifcviewer EXCLUDE_FROM_ALL and consumes only IfcViewerCore.

src/ifcviewer/CMakeLists.txt now gates the wgpu-native fetch + the
Qt-using IfcViewer target + install commands behind NOT EMSCRIPTEN.
The wgpu_native link target still resolves under Emscripten as an
INTERFACE library that activates --use-port=emdawnwebgpu (Dawn's
webgpu.h, replaces the legacy -sUSE_WEBGPU=1).

Build:
    source path/to/emsdk_env.sh
    emcmake cmake -S src/ifcviewer-web -B build-web -G Ninja
    ninja -C build-web
    python3 -m http.server --directory build-web 8080
    # open http://localhost:8080/IfcViewerWeb.html in a WebGPU-capable
    # browser (Chrome 113+, Edge 113+).

Phase B step 3 of #45.
2026-06-04 18:58:34 +10:00
Gorgious56 25651a1507 Fix demo preset crash + scope header refresh
bpy.ops.bim.new_project(preset='demo') crashed in
refresh_bim_tool_headers: the post-commit hook fired for every
nested bpy.ops.bim.append_library_element during template
loading, and the operator context Blender hands to
programmatically-invoked nested operators is stripped of the
view-layer attributes the refresh reads.

Two changes resolve it.

Gate the header refresh in tool.Parametric.refresh_post_commit
on operator.bl_idname being one of the EDIT_TYPES finish_op
idnames. Only validate-gizmo commits (bim.finish_editing_<name>)
now trigger the refresh; demo-loader and other non-edit
operators skip it. Querying the registry directly is the
canonical signal — string-prefix matching would silently drift
if ParametricObject.finish_op changes derivation.

Harden tool.Blender.get_active_object so its view_layer fallback
also uses getattr; the 150+ callers routed through it now
tolerate stripped contexts. _resolve_bim_tool_context applies
the same defensive pattern to mode / workspace.

Tests:
- test_handler_restricted_context covers get_active_object's
  defensive path and the BimTool-family whitelist (excludes
  annotation, spatial, structural).
- test_handler_forward_compat AST-pins that the gate consults
  EDIT_TYPES (not a string prefix).
- test_wall_header_refresh rewritten — three tests cover the
  gated-by-registry contract: counter bumps for every commit,
  finish_op operators refresh headers, others don't.

Hotkey-driven in-place edits (S_E / C_E) no longer trigger the
refresh — they were caught by the pre-refactor "every commit"
design. Left out of scope; the new skip-non-finish test pins
this as intentional.

Generated with the assistance of an AI coding tool.
2026-06-04 10:42:58 +02:00
Dion Moult c098146c35 Remove Autodesk viewer examples
Remove the bonsaiviewer-autodesk Cargo examples that were used for local UI and dialog experiments.

Generated with the assistance of an AI coding tool.
2026-06-04 18:34:46 +10:00
Dion Moult fe6a0452bf ifcviewer: split out IfcViewerCore static library
Pull the Qt-free / OpenCASCADE-free files out of the IfcViewer target
into a new IfcViewerCore static lib: BufferPool, ChunkPlanner,
InstanceCompose, SidecarCache, StreamingLoader, StreamingThread,
LodBuilder, plus the header-only InstancedGeometry / ModelGpuData /
VertexQuantization / Selection / Visibility headers. IfcViewer PUBLIC-
links IfcViewerCore so existing consumers see no change.

This is the boundary the Emscripten web target will link against —
keeps Qt, IfcGeom, OpenCASCADE, CGAL, and Boost out of the wasm build.
Explicit file list, not glob, because the boundary is the whole point.
2026-06-04 18:29:15 +10:00
Dion Moult cb19f22ee4 BufferPool: drop Qt log dependency
Replace qInfo() growth-event logging with fprintf(stderr,...) so
BufferPool.cpp has no Qt touchpoints. Lets the test target drop its
Qt6::Core link too. Prerequisite for the IfcViewerCore library boundary
the web target will link against.
2026-06-04 18:20:37 +10:00
Dion Moult 1fe4570860 ifcviewer: extract ChunkPlanner + InstanceCompose; add Tier-1 test trio
The chunk planner (Morton sort + greedy pack) and instance composition
(federation × placement matrix chain + world-AABB derive) were inline
helpers in ViewportWindow.cpp. Pulled both out as free-function modules
so the math + lookup logic can be exercised without a Qt window or a
wgpu device. ViewportWindow now delegates; InstanceLookup is a using-
alias to InstanceCompose::InstanceLookup.

Also added an addSubBufferForTesting / clearSubPoolsForTesting seam to
BufferPool so the sub-allocator invariants can be pinned with fake
WGPUBuffer handles. The fakes are never dereferenced; the guard drops
the sub-pools before destructor would call wgpuBufferRelease.

Three new test binaries under src/ifcviewer/tests/, 33 cases / 173
assertions: BufferPool first-fit + alignment + coalescing + multi-
sub-pool isolation; ChunkPlanner Morton split / interleave / stable
sort / greedy-pack monotonicity and single-mesh-oversize; InstanceCompose
identity / translation / order-of-multiplication / large-placement
cancellation against federation false origin / column-major writeback /
findInstance lookup paths.
2026-06-04 17:19:24 +10:00
Gorgious56 94faaa3160 Drop dead Geometry.has_material_styles + sanitation sweep
Two related cleanups bundled because each was too small on its own.

== Drop dead Geometry.has_material_styles duplicate ==

Two parallel has_material_styles implementations existed on HEAD:

* Geometry.has_material_styles (tool/geometry.py:853, added by
  3483683cb "Add tool.Geometry helpers for body representation +
  placement"): checks each material via tool.Material.get_style
  for an IfcSurfaceStyle. This is the implementation gizmos-8088
  uses — its core/root.py:58 calls geometry.has_material_styles.

* Root.has_material_styles (tool/root.py:75, added by e76455913
  "Route _has_material_styles through tool.Root.has_material_styles"):
  checks each material for a HasRepresentation inverse. Added to
  fix the test/core/test_root.py::TestCopyClass::test_AAAAAAAAAAAA
  failure by routing the check through a Prophecy-mockable seam.

HEAD's core/root.py:59 calls root.has_material_styles. The Geometry
version became orphaned by that migration — zero callers historically
(git log -S "Geometry.has_material_styles" returns nothing). The
Root placement is the right architectural home: has_material_styles
pairs with assign_body_styles in the copy_class flow as "is there
material-defined styling? if not, apply body styling" — both
decisions live on the same interface, called in sequence from the
same caller.

The semantic delta (HasRepresentation vs IfcSurfaceStyle) is a close
approximation in real IFC files where HasRepresentation almost always
indicates a styled material; if precision becomes necessary, the
Root impl can be tightened independently of this cleanup.

Drop the Geometry method + its abstract declaration in core/tool.py.

== Sanitation sweep per CLAUDE.md §4a ==

Eight rot-prone references in code we authored on this branch get
their first-draft mistakes cleaned up. The §4a rule (no sibling
symbol names, no test paths, no motivation history in docstrings)
got added during this branch, so older commits sometimes named their
siblings in prose; this is a focused cleanup of the worst offenders.

* bim/module/model/wall.py:201 — _CommitWallDraftsFirstMixin
  docstring carried motivation history ("...that every multi-wall
  operator … used to repeat at the top of _execute"). Rewrite to
  describe only the current contract.

* bim/module/model/wall.py:1910 — cycle_type_operator comment named
  two sibling methods. Rephrase to describe what happens at the slot.

* bim/module/model/wall.py:2025 — _active_instances ClassVar comment
  named WallGizmoPreviewDecorator. Rephrase to "the wall-gizmo
  preview decorator" (role, not class).

* bim/module/drawing/gizmos.py:3402 — GizmoFillet hit_uses_bbox
  comment named GizmoWallJoinIntersection. Rephrase to "the wall-join
  gizmo group".

* bim/module/drawing/gizmos.py:3887 — GizmoCountLabel docstring had
  a :meth:`set_count` cross-reference. Drop — reader sees the method
  next to the class.

* bim/module/model/host_add_opening_gizmo.py:201 — poll-exclusion
  comment named GizmoWallEdition + GizmoRoofEdition. Rephrase to
  describe why we skip ("walls and parametric roofs both render
  their own toggle in the pen row").

* bim/module/void/operator.py:45 — preserve_placement comment named
  FilledOpeningGenerator.generate. Rephrase to "the filling-opening
  generator gates its snap-to-wall-axis block on this flag".

* bim/parametric_lifecycle.py:64 — module docstring named the test
  file path (test/bim/test_parametric_registry.py). Rewrite to
  "enforced by the registry contract tests".

Sweep otherwise clean: no third-party software names in this-branch-
authored comments (upstream Revit / Tekla / ArchiCAD references are
legitimate external-constraint workarounds, §4a-allowed). No
PR/issue numbers we authored except the FIXME(PR5) in
tool/parametric.py:150, deliberately preserved until PR6's MEP slice
resolves it.

Generated with the assistance of an AI coding tool.
2026-06-04 09:08:47 +02:00
Dion Moult 749476d1a7 docs: rewrite stale GL-era docs (env-vars + viewport_architecture)
Two long-stale docs that described the deleted OpenGL backend are
replaced with current-state rewrites under `src/bonsaiviewer/docs/`
and wired into the toctree. The originals are removed.

## env-vars.rst (replaces src/ifcviewer/settings.rst)

The orphan `src/ifcviewer/settings.rst` was written for the OpenGL
backend (`IFC_*` prefix, MDI-specific knobs) and was never wired into
any Sphinx toctree — it sat as a one-off file in the C++ source tree,
undiscoverable from a normal docs build.

* **Dead — dropped entirely.** `IFC_SKIP_MDI`, `IFC_MAX_SUBDRAWS`,
  `IFC_SUBDRAW_DIAG` were GL-only `glMultiDrawElementsIndirect`
  instrumentation. wgpu has no MDI. `IFC_FPS_HITCH_MS` no longer
  exists in source.
* **Renamed.** `IFC_HIZ_MOTION` → `WGPU_HIZ_MOTION`,
  `IFC_CULL_THREADS` → `WGPU_CULL_THREADS`.
* **New, previously undocumented.** Ten `WGPU_*` vars added during
  the port + bring-up (WGPU_HIZ, WGPU_HIZ_TRACE, WGPU_MIN_PX,
  WGPU_MIN_PX_MOTION, WGPU_FLY_DEBUG, WGPU_NAV_PRESET,
  WGPU_PRESENT_MODE, WGPU_STREAM_DEBUG, WGPU_STREAM_DEEP_DEBUG,
  WGPU_STREAM_EVICT_LOG). Descriptions written from each variable's
  use-site so wording matches actual behaviour.
* **LOD-build section kept verbatim.** IFC_LOD_ERROR, IFC_LOD_RATIO,
  IFC_LOD_MIN_SAVINGS, IFC_LOD_DEBUG — sidecar-bake knobs,
  backend-agnostic.
* **GUI-promoted "old IFC_* graveyard" section dropped.** The file
  is an env-var reference, not a record of historical spellings.

## viewport_architecture.rst (replaces src/ifcviewer/README.md)

The 994-line `src/ifcviewer/README.md` was an archive of the GL-era
phase-by-phase perf narrative. ~95% of it described deleted code:
OpenGL 4.5 Core, `glMultiDrawElementsIndirect`, VAO/VBO/EBO,
`GL_ARB_shader_draw_parameters`, BVH-per-model, sidecar v5/v7/v9
(current is v13), the now-non-existent `./IfcViewer` binary, Phase
3F "static batching next" plans superseded by the chunk-pool
architecture, Phase 3E "GPU compute culling removed" since re-added
as task #17 pending. Salvaging the ~50 lines of still-correct
content would have left a Frankenstein doc internally contradicting
itself.

Replaced with a focused architecture page covering current reality:
consumer split (BonsaiViewer shell vs IfcViewerMinimal standalone),
stack (wgpu-native v29, Qt6, IfcOpenShell, IfcUtil, Eigen3,
meshoptimizer), five core ideas (unique-mesh instancing, quantized
12 B vertex, chunked streaming on a probed VRAM pool, sidecar v13
fast path, event-driven rendering), per-frame pipeline (cull →
upload → streaming → opaque pass → transparent pass → edge → overlay
→ present), federation + false-origin compose, file map limited to
files that actually exist in `src/ifcviewer/` today, build/run via
`build_viewer.sh`, cross-refs to env-vars.rst, debug-output.rst,
and connectors/.

## Toctree

`src/bonsaiviewer/docs/index.rst` gains `env-vars` and
`viewport_architecture` entries alongside the existing
`connectors/index` and `debug-output`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:57:40 +10:00
Dion Moult ce7d2fa329 docs: split Autodesk connector docs into user + developer pages
`autodesk.rst` had grown to mix end-user concerns (where do my tokens
live, how do I install the bundle, why isn't sign-in working) with
developer concerns (cargo build, fmt/clippy/test, packaging script
flow, per-OS toolchain notes, CI). Reorganise into:

* **`autodesk.rst`** — Autodesk Connector. User-facing. Bonsai-Viewer-
  level intro (Forma/APS/Docs, "Add from cloud"); install-from-zip
  per OS; first-run setup (client ID, OAuth port, browser redirect);
  where settings / cache / OAuth tokens live; proxy / TLS guidance
  for corporate installs.

* **`autodesk_development.rst`** — Autodesk Connector Development.
  Developer-facing. Tech stack (FLTK, ureq, keyring, dirs, serde,
  chrono, webbrowser); `cargo build --release`; `cargo test
  --all-features` / clippy / fmt-check; protocol probing via stdio
  pipe; packaging via `packaging/build.py`; per-OS build / keychain
  / codesign notes; CI workflow overview. Absorbs the entirety of
  the old `autodesk_packaging.rst`, which is removed.

`connectors/index.rst` toctree updated: `autodesk_packaging` →
`autodesk_development`. `cloud_sync_protocol.rst` untouched —
language-agnostic protocol spec.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:57:40 +10:00
Dion Moult 9d9f4054f6 bonsaiviewer-autodesk: replace Python connector with the Rust impl
The Python implementation of the Autodesk Forma connector
(bonsaiviewer_autodesk/) is deprecated. The Rust port that's been
maturing under src/bonsaiviewer-autodesk-rs/ is now the connector
and takes over the original folder name.

## File operations

* `git rm -r src/bonsaiviewer-autodesk` — drop the 18 tracked Python
  source/test/packaging files. (~6.5k untracked build artefacts in
  venv/build/dist/egg-info are removed too, but those were never in
  the index.)
* `mv src/bonsaiviewer-autodesk-rs src/bonsaiviewer-autodesk` —
  the Rust impl takes over the canonical folder name.
* `rm -rf src/bonsaiviewer-autodesk-rs-egui` — abandoned egui-based
  experiment, never committed.
* `src/bonsaiviewer-autodesk/.gitignore` extended with `/dist` to
  keep packaging output out of the index alongside the existing
  `/target` rule.

The Rust binary in Cargo.toml already has `name = "bonsaiviewer-
autodesk"` and `connector.json`'s `exec` field already points at that
name — so the connector loader, build_viewer.sh symlink, and
win/build-all-win.py CONNECTOR_DIR all keep working without edits.

## Packaging shape preserved

`packaging/build.py` is rewritten to:

  * shell out to `cargo build --release` instead of pyinstaller,
  * copy the produced binary + connector.json into the same
    `dist/autodesk/` layout the PyInstaller flow produced,
  * zip into `dist/autodesk-<os>-<arch>.zip` with the same
    naming pattern (CI artifact uploads keep working).

The Rust binary statically links its deps, so unlike PyInstaller
there's no `_internal/` directory — single executable inside
`dist/autodesk/`. Everything downstream (`build_viewer.sh` symlink,
`win/build-all-win.py collect_connector_files`, the zip step in
`build_rocky.yml`) only cares that `dist/autodesk/` exists, so the
on-disk contract is preserved.

Verified locally: `python3 src/bonsaiviewer-autodesk/packaging/build.py`
produces `dist/autodesk/{bonsaiviewer-autodesk, connector.json}`
(3.9 MB stripped ELF) and `dist/autodesk-linux-x86_64.zip` (~1.5 MB
compressed).

## CI updates

* `.github/workflows/build_rocky.yml` and `build_rocky_arm.yml`:
  drop the `pip install ".[build]"` step — `packaging/build.py` is
  stdlib-only now, the cargo build wrapped inside it does the work.
* `.github/workflows/build_win.yml`: same — drop pip install,
  packaging script handles cargo internally.
* `.github/workflows/build-bonsaiviewer-autodesk.yml`: full rewrite
  of the dedicated connector test/build workflow. Replaces the
  Python {3.11, 3.13} test matrix with `cargo fmt --check`,
  `cargo clippy --all-targets -- -D warnings`, and `cargo test
  --all-features`. The OS/arch build matrix is unchanged
  (linux-x86_64, macos-arm64, macos-x86_64, windows-x86_64) but
  installs a Rust toolchain via dtolnay/rust-toolchain@stable and
  caches target/ via Swatinem/rust-cache.

`win/build-all-win.py` and `build_viewer.sh` are unchanged — they
only reference the `dist/autodesk/` path, which the new
`packaging/build.py` populates identically.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 13:59:43 +10:00
Dion Moult b3fbcd6a66 refactor: extract src/ifcutil/ from src/ifcviewer/ (Unit, Geolocation, Placement)
Unit / Geolocation / Placement are schema-agnostic IFC helpers ported
from ifcopenshell.util.{unit,geolocation,placement}. Nothing about
them is viewer-specific: pure IfcParse + Eigen, no Qt, no IfcGeom, no
renderer. Living under src/ifcviewer/ implies an unwanted dependency
direction every time a non-viewer caller (test_federation, the bonsai
SettingsView georef readout, a future standalone IFC tool) wants to
use them.

Move them to a new `src/ifcutil/` static lib (IfcUtil). The lib has
PUBLIC `target_include_directories(${CMAKE_CURRENT_SOURCE_DIR})` so
callers that link IfcUtil can keep `#include "Unit.h"` etc. without
relative-path adjustments — the include dir propagates transitively
via IfcViewer's PUBLIC link.

## Changes

* `git mv src/ifcviewer/{Geolocation,Placement,Unit}.{h,cpp}
   → src/ifcutil/` (history follows the rename).
* `src/ifcutil/CMakeLists.txt`: IfcUtil static lib, PUBLIC links
  IfcParse + Eigen3::Eigen, PUBLIC include dir.
* `cmake/CMakeLists.txt`: `add_subdirectory(../src/ifcutil ifcutil)`
  before ifcviewer/ so the link target exists when IfcViewer's
  CMakeLists runs.
* `src/ifcviewer/CMakeLists.txt`: IfcUtil added to IfcViewer's PUBLIC
  link_libraries.
* `src/ifcviewer/tests/CMakeLists.txt`: test_federation drops the
  explicit `${IFCVIEWER_SRC}/{Unit,Geolocation,Placement}.cpp`
  source list and links `IfcUtil` instead (matches how production
  code resolves the symbols).
* `src/bonsaiviewer/modules/models/SettingsView.cpp`: the two
  explicit `#include "../../../ifcviewer/{Geolocation,Unit}.h"`
  paths swap to `../../../ifcutil/…`. All other callers use bare
  `#include "Unit.h"` style and continue to work via the propagated
  include dir.

## Verification

* `ninja -C build-viewer` builds clean: IfcUtil + IfcViewer +
  IfcViewerMinimal + BonsaiViewer + all four pre-existing
  ifcviewer tests + the two from-wgpu tests.
* `test_federation` runs green: 226 assertions in 22 test cases
  pass with IfcUtil linked instead of the explicit-source compile.
* `git log --follow` traces e.g. `Geolocation.cpp` back through the
  rename to its prior location in src/ifcviewer/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 13:25:28 +10:00
Dion Moult 32d9fd6c1c build-all: restore full PYTHON_VERSIONS list
d390911d75 ("build_osx: build BonsaiViewer on macOS via build-all.py",
2026-06-01) accidentally committed a local single-version pin
(`PYTHON_VERSIONS = ["3.11.8"]`) intended only for fast iteration
during macOS bring-up. With macOS / CI green and the Python wrapper
fix from 748b4e72a landed, restore the full multi-version list so
both Rocky and macOS CI publish wrappers for 3.10/3.11/3.12/3.13/3.14
again.

Cost is ~5 from-source Python builds per CI run; the cache-deps
plumbing in nix/cache_dependencies.py already memoises these so
repeated runs only pay it once per Python release bump.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 13:04:14 +10:00
Dion Moult b123ee69d6 viewer: two-pass alpha transparency + Alt+X global x-ray cap
## The bug

FZK-Haus windows rendered fully opaque despite every piece of the
data path carrying alpha correctly: vertex format is RGBA u8x4,
InstanceCpu/InstanceGpu carry color_override_rgba8 with its alpha
byte, fs_main returns vec4(rgb, in.color.a). Cause: the main render
pipeline's color target had `blend = nullptr`, which in wgpu disables
the blend stage entirely — fragment RGBA overwrites the back buffer
unmodified, alpha discarded.

## Why "just enable blend" isn't enough

Two failure modes that don't go away with a one-liner:

1. `depthWriteEnabled = True` on the main pipeline would make a
   transparent window-frame pane occlude geometry behind it in
   depth, so the wall behind the window then fails the depth test
   and never draws — you'd see the silhouette of the window with
   whatever colour was in the back buffer before, not the wall.
2. Order-dependent blending across transparent surfaces in arbitrary
   cull order — overlapping transparent surfaces would shift colours
   as the camera moves.

Standard fix for a BIM viewer is two-pass opaque-then-transparent.

## What this commit adds

### Per-mesh "has any alpha < 255" classifier
* `ModelGpuData::mesh_has_alpha` (uint8_t vector, parallel to meshes).
* Sized in `applyCachedModel`.
* Populated in `applyStreamedChunk` by scanning each in-chunk mesh's
  vertex bytes for a vertex's alpha byte < 255 (offset 11 within
  the 12-byte vertex record — the 4th byte of the third u32, which
  the shader reads as `w2 >> 24`). Single chunk-arrival site covers
  both sidecar streaming and the worker-result drain. First-load
  IFC-without-sidecar geometry still routes opaque until the sidecar
  bake completes; A-path scan is deferred.

### Per-chunk opaque/transparent partition during cull
* `Chunk::opaque_visible_vertices` / `opaque_visible_draws`
  (per-frame counts).
* Transient `visible_draws_scratch_transparent` +
  `transparent_per_draw_vertex_counts` filled alongside the existing
  opaque half during the cull walk. Post-walk concat appends
  transparent entries onto the opaque half and continues the
  cumulative prefix-sum sequence — single buffer, single bind
  group, no doubling.
* Classifier inside the cull lambda:
    `xray_active ? always_transparent
     : override_active ? (override.alpha < 255)
     : mesh_has_alpha[mesh_id]`

### Per-chunk uniform layout extension
From `[total_draws, total_verts, 0, 0]` to
`[total_draws, total_verts, opaque_verts, opaque_draws]`. The third
slot is what `render()` passes as `firstVertex` to the transparent-
pass draw call so the shader's vid lands in the transparent range of
the same visible_draws_scratch buffer.

### `main_pipeline_transparent_`
Copy of `main_pipeline_` with `color_target.blend = SrcAlpha /
OneMinusSrcAlpha`. depthWriteEnabled stays True (see below).

### Two-pass `render()`
Opaque pass (`main_pipeline_`, firstVertex=0,
vertexCount=opaque_visible_vertices) then transparent pass
(`main_pipeline_transparent_`, firstVertex=opaque_visible_vertices,
vertexCount=total - opaque). Each loop skips empty halves so an
opaque-only chunk costs one draw call, transparent-only one draw,
mixed chunks two.

### depth_transparent.depthWriteEnabled = True (NOT off)

Initially set False (standard "let further-back geometry paint
through transparent front faces" trick) but that broke the edge-
detect pass: edge detection reads the depth buffer to find
silhouette discontinuities, and windows-without-depth meant the
glass had no silhouette at all (panes looked like framed holes) and
the edges of opaque geometry behind the glass painted through at
full intensity. Keeping the write avoids that — trade-off is depth-
test occlusion between transparent surfaces (closer occludes
farther), which for BIM panes that don't overlap in screen space
is invisible. Real fix for the overlap case is OIT or sort-back-
to-front, not depth-write toggling.

## Alt+X global X-ray (drops in basically free)

* `xray_alpha_cap` field on FrameUniforms + WGSL counterpart, default
  1.0 (no effect). fs_main clamps `out.a = min(in.color.a, cap)`.
* `ViewportWindow::xray_alpha_cap_` member, default 1.0. Alt+X
  toggles between 1.0 and 0.3.
* Cull classifier sees `xray_alpha_cap_ < 1.0` and forces every
  instance into the transparent pass so the blend stage actually
  fires (an opaque-pass fragment with capped alpha would still
  overwrite the back buffer).
* No per-instance state mutation needed — toggle is a single float
  in a uniform plus a re-cull. Excluding objects from x-ray later
  would mean tagging them so the classifier skips the force-
  transparent branch for them, also small.

Stress-tested on FZK-Haus: window glass visibly translucent with
correct silhouette edges; Alt+X turns the whole scene to a tinted
ghost of itself and back without artefact.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 12:58:50 +10:00
Dion Moult 748b4e72a9 macOS: re-enable Python wrapper + stage IfcViewerMinimal.app bundle
Three coupled fixes that close the macOS bring-up loop:

## 1. ifcwrap: fix INSTALL_RPATH on Apple

The ifcopenshell_wrapper Python module had `INSTALL_RPATH "$ORIGIN"` set
for "NOT WIN32 AND NOT WASM_BUILD" — but `$ORIGIN` is a Linux ld.so
placeholder, not a macOS dyld one. macOS dyld doesn't expand it; it
bakes the literal string `$ORIGIN` into LC_RPATH, which resolves to
nothing at runtime. The wrapper's hard-link `@rpath/ifcopenshell
.document.rdb.dylib` then fails to load even though INSTALL(TARGETS …
LIBRARY DESTINATION "${python_package_dir}/ifcopenshell") above had
already dropped the plug-in dylib right next to the wrapper.

Split the rpath assignment: `@loader_path` on Apple (the dyld
equivalent of `$ORIGIN`), `$ORIGIN` elsewhere.

This is what b0ef47819 (the build_osx IFCOS_BUILD_PYTHON_WRAPPER=off
gate) was working around. The gate is removed below.

## 2. ifcviewer-minimal: stage IfcOpenShell + wgpu_native into the .app

IfcViewerMinimal.app was building on macOS via the cmake
`BUILD_BONSAIVIEWER → BUILD_BONSAIVIEWER_WGPU` promotion, but had no
bundle staging — Contents/Frameworks/ only contained the Qt
frameworks macdeployqt deposited, so the .app would refuse to start
("Library not loaded: @rpath/libwgpu_native.dylib").

Mirror what src/bonsaiviewer/CMakeLists.txt does for BonsaiViewer.app:

* Set INSTALL_RPATH to `@executable_path/../Frameworks` so the exe
  knows where to look for @rpath/* deps.
* install(FILES) libwgpu_native.dylib into the bundle's Frameworks/
  (globbed from WGPU_NATIVE_LIB_DIR rather than hard-coded so it
  covers any future versioned name).
* install(CODE) staging block that copies every `*.dylib` from
  <prefix>/lib/ into the bundle's Frameworks/, excluding the
  geometry-writer plug-ins (same EXCLUDE regex as BonsaiViewer.app —
  viewer doesn't need OBJ/glTF/DAE/STP/IGS/SVG/TTL export converters).

Same long-form rationale + caveats apply (macdeployqt doesn't follow
non-Qt @rpath deps, lib-prefixed core libs vs ifcopenshell.* plug-in
naming split, Linux's equivalent lives in build_rocky.yml workflow
bash via patchelf + stage_runtime_payload). See src/bonsaiviewer/
CMakeLists.txt for the full version.

## 3. build_osx.yml: drop IFCOS_BUILD_PYTHON_WRAPPER=off

With (1) fixed, the Python wrapper smoke test should pass again. The
gate goes away; the comment block in build_osx.yml is replaced with a
short note pointing at the ifcwrap rpath fix as the underlying change
that re-enables this.

Together, (1)+(2)+(3) close the standalone IfcViewerMinimal-on-macOS
gap (task #43) and re-enable IfcOpenShell-Python on the macOS arm64 CI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 11:31:45 +10:00
Dion Moult 8ab5c31e75 refactor: merge ifcviewer-wgpu into ifcviewer, drop Wgpu prefix
The GL backend is gone (task #53). The wgpu/non-wgpu folder split and
the Wgpu* class prefix were both disambiguation artefacts from the
overlap period — now pure dead weight.

## Folder + library merge

* `src/ifcviewer-wgpu/`  → folded into `src/ifcviewer/` (git mv tracks
  every file as a rename so blame/log history survives).
* `src/ifcviewer-wgpu-minimal/` → `src/ifcviewer-minimal/` (the exe was
  already named `IfcViewerMinimal`; this just brings the folder + CMake
  target name into line).
* `src/ifcviewer-wgpu/tests/test_wgpu_{selection,visibility}.cpp` →
  `src/ifcviewer/tests/test_{selection,visibility}.cpp`, folded into
  the existing `add_ifcviewer_unit_test(...)` helper.
* The `IfcViewerWgpu` static library is dissolved — its sources become
  part of the unified `IfcViewer` static library, which now bundles
  scene/loader + renderer in one target. The pre-merge circular
  dependency (IfcViewer linking IfcViewerWgpu just to get the
  ViewportWindow.h include path that SceneLoader.h needs) goes away.
* The wgpu-native FetchContent block, the Cocoa/QuartzCore link on
  Apple, the OBJCXX-enabled `.mm` source, and the wgpu-native runtime
  install all move into `src/ifcviewer/CMakeLists.txt` unchanged.

## Type renames (Wgpu prefix dropped from every Wgpu* identifier)

  WgpuAreaMeasurement   → AreaMeasurement
  WgpuBufferPool        → BufferPool
  WgpuLengthMeasurement → LengthMeasurement
  WgpuMetalSurface      → MetalSurface
  WgpuModelGpuData      → ModelGpuData
  WgpuOverlayFrame      → OverlayFrame
  WgpuOverlayRenderer   → OverlayRenderer
  WgpuSectionPlane      → SectionPlane
  WgpuSelectionState    → SelectionState
  WgpuStreamingLoader   → StreamingLoader
  WgpuStreamingThread   → StreamingThread
  WgpuViewportWindow    → ViewportWindow
  WgpuVisibilityState   → VisibilityState

  CMake target IfcViewerWgpuMinimal → IfcViewerMinimal (exe name was
  already this since wgpu shipped as default).

Deliberately kept: `onWgpuLog` (wgpu-native log callback — names a
binding to an external API, not one of *our* types), and the WGPU*
enum/struct prefixes from wgpu-native's own headers. `WgpuMemProbe`
lives in the separate `src/wgpu-mem-probe/` standalone diagnostic
project and isn't touched.

## Include-path updates

Every `#include "../ifcviewer-wgpu/Wgpu<X>.h"` → `"../ifcviewer/<X>.h"`,
every in-directory `#include "Wgpu<X>.h"` → `"<X>.h"`. Includes from
sibling subdirectories (modules/, etc.) are updated to point at
`../../../ifcviewer/` instead of `../../../ifcviewer-wgpu/`.

## cmake/CMakeLists.txt simplification

The redundant `add_subdirectory(ifcviewer-wgpu)` blocks (one inside
the BUILD_BONSAIVIEWER fan-in, one in the BONSAIVIEWER-less standalone
block) collapse into a single unconditional
`add_subdirectory(../src/ifcviewer ifcviewer)`. The standalone block
keeps only `wgpu-mem-probe` (the diagnostic tool, unrelated to the
viewer lib).

## Verification

* Full build green: `IfcViewer` static lib, `IfcViewerMinimal` exe,
  `BonsaiViewer` exe, all four pre-existing ifcviewer unit tests, and
  the two new-location tests (`test_selection`, `test_visibility`).
* No stray `Wgpu<X>` identifier remains across `src/ifcviewer/`,
  `src/bonsaiviewer/`, `src/ifcviewer-minimal/` (verified by grep).
* Renames tracked by git as `R` entries — `git log --follow` on
  ViewportWindow.cpp etc. continues to show history through the move.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 11:11:14 +10:00
Dion Moult 7f87408b78 bonsaiviewer: rework add-models false-origin guess + drop fly-mode input lag
Two independent threads that landed on this branch.

## 1. Federation false-origin guess: arm-on-add + frame-on-origin

The cc54237f5 fix moved the guess off the refresh() fan-in (terminating
the stack-overflow recursion on Holter Tower), but the gating was still
`modelIds().size() == 1` — broken for batch-add-into-empty-session
because the session registry is populated synchronously by all
addModel calls before any modelGeometryReady lands. Result: load 3
files at once → none of the per-model geometry-ready events ever
finds `size() == 1` → guess never fires → camera + federation origin
stay at surveyor coords.

Rework:

* **Arm/consume at the command boundary.** modules/models/Commands
  exposes `armFederatedFalseOriginGuess` / `consumeFederatedFalseOrigin
  Guess` (function pair — arming is a one-shot, raw-bool would let a
  peek-without-clear silently break the contract). `addModel` and the
  cloud-callback in `addModelFromCloud` arm if `modelIds().isEmpty()`
  at the moment they're about to register federation entries. The
  first geometry-ready then consumes the arm and runs the guess —
  batch add or single, works the same way.

* **Lazy first geometry point from mesh AABB centre.** Drop
  SceneLoader's `firstPlacement(mid)` / `first_placement` /
  `has_first_placement` and the two capture sites entirely. The
  viewport keeps CPU-side MeshInfo + InstanceCpu for picking /
  measurement; compute the anchor on demand via new const accessor
  `WgpuViewportWindow::firstGeometryPointWorldM(mid, out)` =
  instance0.placement × meshes[instance0.mesh_id].aabb_centre.
  This is more representative than the placement translation
  (placements often live far from the actual geometry due to long
  ObjectPlacement chains / intermediate local frames), and lighter
  storage-wise (lazy, vs. 128 B per model held just-in-case).

* **`guessFederatedFalseOrigin` math signature: Matrix4d → Vector3d.**
  The function only ever consumed `.block<3,1>(0,3)`; the Matrix4d
  API surface was a strictly-larger-than-necessary contract. Vector3d
  matches what the function actually needs.

* **`WgpuViewportWindow::frameOnFederatedOrigin(mid, max_distance_m)`**
  replaces the post-shift use of viewAll() that the ViewportView
  almost reached for. The federated false origin sits at (0,0,0) in
  post-shift space by construction, so the camera targets there
  directly; distance fits the model's post-shift AABB diagonal with
  viewAll's padding math, clamped to `max_distance_m` so a model
  with one crazy-coord outlier vertex can't pull the camera back so
  far the real geometry becomes a pixel. Called with 100 m cap from
  the guess. Unlike viewAll() this only iterates the one model the
  guess fired for — the "load 10 models, viewAll shows nothing"
  failure mode is structurally avoided.

* **ViewportView::tryGuessFirstModelFalseOrigin** renamed to
  `guessFederatedFalseOriginFromFirstModel` and the body restructured
  to consume the arm, look up the anchor via the viewport, mutate
  the federation origin (which propagates through SessionState's
  federatedFalseOriginChanged relay → refresh() → recompose all
  instance world AABBs), then `frameOnFederatedOrigin(mid, 100)`.

* **Internal guards preserved.** filePath skip (project files own
  the origin), current==defaults skip (don't clobber a user who
  set the origin manually then removed the model), placement /
  georef availability checks — defense-in-depth around the arm, not
  the primary gate. The arm-only flow means re-arming on add-into-
  empty-session is naturally re-firable: add → remove → add will
  retry if the previous guess returned defaults.

## 2. wgpu present-mode: prefer Immediate above FifoRelaxed

On Linux Vulkan stacks where the driver / compositor doesn't advertise
Mailbox (confirmed on the user's setup — capability log added in this
patch reports just `fifo, fifo_relaxed, immediate`), Fifo's 2–3 frame
queue doubles input-to-photon latency the moment WASD activates
(~16 ms render-body fully consumes the budget, so the queue is held
deep). On a 60 Hz display this reads as "less smooth than the 100 fps
HUD suggests" during fly-mode mouse-look-while-moving — confirmed by
WGPU_FLY_DEBUG dt traces (rock-solid 16-17 ms cadence, so it's not
frame pacing — it's latency).

Promote Immediate above FifoRelaxed in the preference order so that
when Mailbox is unavailable we pick the no-queue option (can tear
under fast motion, but tearing on architectural geometry is usually
invisible while the latency win is immediately felt). Also log the
full advertised capability list on first configure so future "why
isn't Mailbox available?" diagnostics don't need a code patch.

Mailbox remains first preference; Fifo remains the spec-guaranteed
final fallback.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 10:02:53 +10:00
Gorgious56 0e922074b9 Adopt _CommitWallDraftsFirstMixin on 7 wall operators
The 7 multi-wall operators (UnjoinWalls, UnjoinWallPathConnection,
ExtendWallsToUnderside, ExtendWallsToWall, SplitWall, MergeWall,
JoinWallsIntersection) each opened their _execute with an identical
prologue:

    _commit_pending_wall_edits_for_selection(context)
    # ... operator-specific logic

— flushing any in-progress wall parametric drafts so the operator
acts on committed IFC state rather than the draft preview box.

Extract that prologue into _CommitWallDraftsFirstMixin: its _execute
calls the commit helper, then delegates to a subclass-supplied
_perform. Subclasses inherit the mixin first in their bases tuple so
the mixin's _execute resolves first via the MRO. The IFC transaction
opened by tool.Ifc.Operator.execute still wraps both the commit and
the perform.

Behaviour-equivalent — same call, same order, same selection scope.
Architectural cleanup only: a future multi-wall operator can no
longer forget the commit step. The named helper
_commit_pending_wall_edits_for_selection stays as the single
encapsulation of the names=("wall",) filter; its docstring loses
the stale "every multi-wall operator calls it at the top of
_execute" sentence and now just describes the filter contract.

Matches gizmos-8088's _CommitWallDraftsFirstMixin pattern.

Generated with the assistance of an AI coding tool.
2026-06-03 17:15:45 +02:00
Gorgious56 90ea256cc3 Shift-click add-opening preserves filling placement
The regular bim.add_opening click on the host-add-opening gizmo
(wall + door/window co-selected) routes through
FilledOpeningGenerator.generate, which snaps the filling to the
wall's reference-line axis, optionally rotates 180° when the
filling sits on the opposite side, and re-applies an rl1 / rl2
Z-elevation default. That is the right default for "drag a fresh
door onto a wall and let the model place it for me", but defeats
the workflow where the user has already positioned the filling
precisely (e.g. snapped to a window in an adjacent wall, copy-
pasted at an exact Z, aligned to a reference object).

Holding SHIFT while clicking the gizmo now opts into a
"preserve placement" mode: the filling stays at its current
matrix_world and the opening is created at the filling's existing
position. The opening / filling rels and representation work are
unchanged — only the snap-to-axis branch is skipped, so the IFC
graph is identical to the regular click; only the spatial
position of the filling differs (user-chosen vs auto-snapped).

Implementation:

* bim/module/void/operator.py: AddOpening gains a hidden
  preserve_placement BoolProperty + an invoke() that sets it from
  event.shift. The call into FilledOpeningGenerator.generate
  forwards the flag. bl_description documents the SHIFT modifier
  so it surfaces in F3 search / hover tooltip.

* bim/module/model/opening.py: FilledOpeningGenerator.generate
  accepts preserve_placement (default False — backwards-compatible
  with the other caller, tool.Model.add_filled_opening). The
  voided_obj.data-gated snap block (raycast + axis projection +
  rl-Z default + filling_obj.matrix_world write) skips entirely
  when the flag is True. The opening's matrix_world reads from
  filling_obj.matrix_world below the gate, so the opening lands
  at the filling's preserved position automatically.

Generated with the assistance of an AI coding tool.
2026-06-03 16:44:38 +02:00
Gorgious56 387bd51b4a Use menu pick gizmo for door / window / stair type
The door / window / stair edit-row's type-cycle icon advanced one
type per click (CycleDoorType / CycleWindowType / CycleStairType
bound to cycle_type_operator). DoorType has 8 IFC variants,
WindowType 9, StairType 3 — so cycling past the target was the norm.

Threshold rule for cycle-vs-menu: cycle is appropriate for exactly 2
values (advance-one-per-click stays predictable). Three or more
values warrants a popup menu. Door / window / stair all qualify;
roof (RoofGenerationMethod has 2 values) keeps cycle. Wall has no
type cycle. Array is unaffected.

Swap to the popup-menu pattern (PickTypeMixin already on HEAD at
bim/parametric_lifecycle.py:442): clicking the icon opens a menu
listing all type_literal values; selecting one applies it in a
single undo step. The hamburger icon (VIEW3D_GT_menu) is wired into
BaseParametricGizmoGroup.setup_editing_gizmos whenever
pick_type_operator is set (mutually exclusive with
cycle_type_operator). Matches gizmos-8088's pattern exactly.

Per-feature shape:

* door.py: PickDoorType replaces CycleDoorType.
  GizmoDoorEdition.cycle_type_operator → pick_type_operator.
* window.py: PickWindowType replaces CycleWindowType. Same swap.
* stair.py: PickStairType replaces CycleStairType (no
  tool.Ifc.Operator inheritance — stair-type changes
  BIMStairProperties only, no IFC mutation). Same swap.
* bim/module/model/__init__.py: registration entries renamed
  Cycle* → Pick*.
* bim/module/drawing/gizmos.py: drop the
  CycleTypeMixin / PickTypeMixin / TypeAccessorBase shim re-export —
  its own docstring already noted "PR5 cleanup drops these" and the
  three callers (door / window / stair Cycle*Type) it served are
  gone. Roof's CycleTypeMixin import was already direct from
  bim.parametric_lifecycle. Also update GizmoMenu docstring to
  reflect the 2-vs-3+ threshold.

Generated with the assistance of an AI coding tool.
2026-06-03 16:07:13 +02:00
Gorgious56 de4c394b50 Add host-wall offset gizmos for door/window edit
When entering parametric edit on a door or window that fills a
wall opening, four dimension gizmos now measure the distances
from the wall edges to the filling's jambs and from the wall's
base/top to the sill/header. Dragging any gizmo translates the
filling along the wall's local axis; 180°-flipped fillings and
slanted LAYER2 walls round-trip correctly. The has_host_wall
predicate hides all four when the filling → opening → wall
chain cannot be resolved.

Generated with the assistance of an AI coding tool.
2026-06-03 15:34:48 +02:00
Gorgious56 ab64b652ff Show wall cursor gizmos outside edit mode + axis previews
Four concerns that together make the cursor-anchored gizmos
(extend_x_gizmo, extend_z_gizmo, split_gizmo on GizmoWallEdition)
fully functional and visually informative without entering parametric
edit mode first:

* Drop the props.is_editing gate in _update_cursor_gizmos. The three
  bound operators (bim.extend_wall_to_cursor,
  bim.extend_wall_height_to_cursor, bim.split_wall_at_cursor) already
  poll on wall-selected and commit any pending wall edit before
  acting, so single-click without entering edit mode is now the
  canonical flow. Matches gizmos-8088's always-on behaviour.

* Register GizmoWallEdition instances in a per-region weakref map
  (_active_instances) populated at setup_element_specific_gizmos
  time. The WallGizmoPreviewDecorator dereferences this map to read
  live is_highlight state off the cursor icons. Without the
  registration its _cursor_icon_hovered always returned False and
  the hover-gated GPU previews silently never drew. Mirrors the
  same pattern already in place on GizmoWallJoinIntersection.

* Add post-operator resync to all three cursor operators
  (_maybe_resync_wall_props_from_ifc for the single-wall split /
  extend-height paths, _resync_walls_after_mutation for the
  selection-wide extend-X path). Without this, props.length /
  props.height stayed stale after the operator ran, so the
  orientation flips _apply_wall_extend_flips computes from
  cursor_local vs wall dimensions kept using the pre-extend values
  until the next selection change. Matches gizmos-8088's pattern.

* Hover-gated GPU previews per icon:

  - extend-X: filled Z=0 floor quads spanning the wall's offset to
    offset+thickness Y band, visible from plan view without side-
    view clutter. Grow case (cursor beyond either endpoint): one
    green decorator_color_selected quad over the extension. Shrink
    case (cursor inside extent): green quad for the portion that
    REMAINS + red decorator_color_error quad for the portion the
    operator REMOVES.

  - extend-Z: vertical lines at the cursor's projected X in the
    wall's y=0 reference-line plane. Grow case (cursor above wall
    top): one green segment from z=height to z=cursor.z. Shrink
    case: green from z=0 to z=cursor.z (REMAINS) + red from
    z=cursor.z to z=height (REMOVES).

  - split: one red vertical line at the cursor's projected X from
    base to wall top — the cut plane.

  Quads use QUAD_ALPHA=0.25 so the underlying wall body stays
  visible.

* New module-level _fill_quads_alpha helper next to
  _stroke_lines_alpha, plus a per-decorator _fill convenience method
  and a _wall_floor_quad corner builder.

Modal-active gizmo hiding (is_gizmo_hidden_by_modal) is preserved.

Generated with the assistance of an AI coding tool.
2026-06-03 13:57:39 +02:00
Gorgious56 99bb1e30ad Generalise opening gizmos + DRY toolbar plumbing
Add openings — GizmoWallAddOpening only fired when a wall was active +
co-selected with a non-host; slabs and roofs got no in-viewport handle.
GizmoHostAddOpening covers all three host types via is_supported_host,
dispatching walls to the axis-projection anchor and slabs/roofs to a
world-Z anchor lifted just above the host's top face (predictable
height regardless of the void's vertical position).

Show openings on hosts with their own parametric-edit toolbar —
GizmoRoofEdition gains an idle-row toggle_openings_gizmo parallel to
the wall's, parked at the cancel-slot X next to the pen. Visible only
when the host carries HasOpenings and the edit triad is idle. Roof
overrides get_element_height to return the mesh's world-AABB top in
object-local Z, so the WHOLE pen-row anchors visibly above sloped or
stepped roof bodies. The wall's idle-row toggle now also hides when
HasOpenings is empty.

Show openings on hosts WITHOUT a parametric-edit toolbar —
GizmoHostToggleOpenings scoped strictly to the fallback case: a single
host selected, HasOpenings non-empty, NOT a path-connectable wall, NOT
a parametric roof. Covers slabs today plus any foreign-authored IfcRoof
without BBIM_Roof. Anchored at object origin XY + world-AABB top Z.
When slab parametric-edit eventually lands, the slab predicate joins
the exclusion list and this gizmo's poll narrows automatically.

Operator move — ToggleWallOpenings was already host-agnostic; renamed
to ToggleHostOpenings in opening.py (bl_idname bim.toggle_host_openings).
Three callers (the wall idle-row binding, GizmoWallFilletToggleOpenings,
and workspace.py's hotkey_A_O for Alt+O) now route through the renamed
operator. The Alt+O binding is surfaced in the operator's
bl_description so it appears in F3 search and hover tooltips.

DRY refactors —
* GizmoWallAddOpening deleted (subsumed by GizmoHostAddOpening)
* tool.Blender.get_object_world_bounding_box added as the world-AABB
  sibling of the existing local helper; 3 inline call sites in
  tool/misc.py (set_object_origin_to_bottom, scale_object_to_height)
  and gizmos.py adopt it (2 other sites in drawing/operator.py and
  project/operator.py inherently need raw transformed corners for
  per-corner plane / NDC tests — not AABB candidates)
* BaseParametricGizmoGroup gains setup_pen_row_toggle_openings_icon +
  update_pen_row_toggle_openings_icon; wall + roof + any future host
  gizmo wire up the idle-row toggle with two one-line calls
* _resolve_active_host shared poll prologue between the two host
  gizmos (gate + selection count + active-in-selected + entity lookup
  + supported-host check)
* HasOpenings non-empty checks at 3 sites route through
  tool.Geometry.has_openings
* hotkey_A_O body collapsed to bpy.ops.bim.toggle_host_openings()

The forward-compat AST guard pinning "must accept fillet-corner walls"
retargets from GizmoWallAddOpening.poll to is_supported_host.

Generated with the assistance of an AI coding tool.
2026-06-03 12:39:17 +02:00
Dion Moult 2b91e41fc4 bonsaiviewer: stage all IfcOpenShell dylibs (core + plug-ins) on macOS
The .app bundle's Frameworks/ staging rule was only globbing
ifcopenshell.*.dylib (the dlopen-only plug-ins) on the assumption
that macdeployqt would follow BonsaiViewer's link-time @rpath deps
for the lib-prefixed core shared libs. In practice it doesn't —
non-Qt @rpath deps whose source path is outside the standard system
/ Qt prefixes get skipped silently.

In a static build this didn't matter: libifcopenshell.geometry,
libIfcParse, libIfcViewer, etc. were statically embedded in
BonsaiViewer.exe, so there was no runtime dep. With --shared (added
in ddee88bed for the bundle-size win) they're separate dylibs that
must physically live in Frameworks/, or the binary won't even start:

    dyld: Library not loaded: @rpath/libifcopenshell.geometry.dylib
      Reason: tried '…/BonsaiViewer.app/Contents/Frameworks/
              libifcopenshell.geometry.dylib' (no such file)

Broaden the install(CODE) glob to *.dylib so both flavours land:

  * lib-prefixed linked core libs (libifcopenshell.geometry.dylib,
    libIfcParse.dylib, libIfcViewer.dylib, lib<mapping/kernel>.dylib)
  * non-prefixed plug-ins (ifcopenshell.parse.schema.ifcXxX.dylib,
    ifcopenshell.geometry.mapping.ifcXxX.dylib, etc.)

<prefix>/lib/ is IfcOpenShell-exclusive (Qt / boost / eigen live in
their own brew / build prefixes), so the broad glob doesn't risk
sweeping in unrelated dylibs. The geometry-writer EXCLUDE regex is
preserved so the size win from the writer-skip side of ddee88bed
stays in place.

In a static build the lib/ directory simply has no *.dylib files
that match, so the rule no-ops cleanly — same code path is safe for
both --shared and the (unused but possible) default static config.

Linux didn't need a counterpart: build_rocky.yml already does the
equivalent in workflow bash (`patchelf --set-rpath '$ORIGIN'` +
`stage_runtime_payload`), and local Linux dev uses CMake's
BUILD_RPATH which auto-resolves to the build subdirectories. macOS
.app bundles are treated as opaque by the packaging step (no
stage_runtime_payload against Contents/), so the staging has to
happen at CMake install time when the bundle is being assembled.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 15:29:59 +10:00
Dion Moult cc54237f51 viewport: move first-model false-origin guess out of refresh()
Loading a model whose first placement sits at the world origin
stack-overflowed BonsaiViewer instantly on Windows (and macOS).
WinDbg trace was a 5-frame Qt signal-slot cycle hitting the guard
page ~1400 levels deep; Linux escaped only because that machine's
iterator order put a non-origin instance first, which made the guess
return a non-default value and naturally terminated the recursion
after one step.

Root cause is the architecture, not the specific guard inside the
guess function. `ViewportView::refresh()` was connected to six
SessionState signals (projectReset, projectOpened, modelsChanged,
federationChanged, visibilityChanged, modelGeometryReady) and was
calling `maybeGuessFederatedFalseOrigin` on every model on every
fire. That helper called `session_state_->notifyFederationChanged()`
unconditionally after the mutation, which re-emitted
SessionState::federationChanged, which re-entered refresh(), which
re-entered the guess — a hidden emit-in-slot loop. The "current ==
defaults" guard at the top of the guess prevented further mutations
once the value moved off defaults, but on machines where the guess
itself returned defaults the guard never fired and the loop ran
forever.

Cleanup:

* refresh() is now terminal: it reads federation state, pushes it to
  the viewport, and returns. No mutations, no signal emissions.
  maybeGuessFederatedFalseOrigin is removed from its for-loop.

* The guess is renamed to `tryGuessFirstModelFalseOrigin(uint32_t)`
  and is now invoked only from the modelGeometryReady connection,
  not from refresh(). Conditions:
    1. modelIds().size() == 1 (the just-loaded model is the only
       model — i.e. this is the "first model added" edge)
    2. federation->federatedFalseOrigin() == defaults (nobody has
       set the origin yet — possibly because the previous attempt
       guessed defaults and no-op'd, in which case we deliberately
       want to retry next time a model lands)
  No one-shot flag: add→remove→add cycles re-attempt the guess
  precisely while the origin is still default, which is the right
  semantics.

* SessionState now relays Federation::federatedFalseOriginChanged
  onto its own bus via notifyFederationChanged. This replaces the
  manual `session_state_->notifyFederationChanged()` call the old
  guess made post-mutation. With the relay in place, any future
  mutation site (commands, settings dialog, project load) will
  propagate to views automatically — the emit point lives at the
  data change, not at every caller. Views still subscribe to
  SessionState only; Federation stays a back-end detail.

Reproduced with ISSUE_053_20181220Holter_Tower_10.ifcview on
Windows (build 21d3945, WinDbg `kn30` showed the recurring cycle
explicitly). Diagnosis confirmed on Linux by adding tracing prints
in refresh() and the guess body: same model, same code, but the
iterator's first-placement happened to be non-origin so the loop
terminated after one step.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 12:19:17 +10:00
Dion Moult ddee88bed3 build_osx: --shared + skip geometry-writer plug-ins (~3x bundle shrink)
Mirrors the Rocky workflow's two-part size reduction (27249770e
"Reduce Rocky package size") on macOS:

1. Pass `--shared` to nix/build-all.py. The default builds
   IfcOpenShell as static libs, which means every plug-in dylib
   (schemas × 8, kernels × 3, mappings × 8, writers × 8, document
   serializers × ~4, linework processing) statically embeds a full
   copy of libIfcParse + libIfcGeom. With --shared the plug-ins
   reference @rpath/libIfcParse.dylib + @rpath/libIfcGeom.dylib and
   the per-plug-in dylib drops from ~30-50 MB to a few MB each.
   Dominant size win.

2. Filter `ifcopenshell.geometry.writer.*.dylib` out of BonsaiViewer's
   plug-in staging step in src/bonsaiviewer/CMakeLists.txt. These are
   the per-schema OBJ / glTF / DAE / STP / IGS / SVG / TTL export
   converters — heavy because each one inlines the full schema, and
   BonsaiViewer is a viewer, never an exporter, so they're pure
   deadweight inside the bundle. Additive on top of --shared.

Bundle went 300 MB → expected ~100 MB, in line with Linux (~100 MB)
and Windows (~80 MB).

The IFCOPENSHELL_BUILD_PYTHON_WRAPPER=off gate is unchanged for now
— once we confirm BonsaiViewer.app size + functionality look sane,
we can ungate the Python wrapper and see if shared-builds-on-macOS
shake out its install issues too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 08:06:42 +10:00
Gorgious56 1707c36bd8 Stack cursor-anchored wall gizmos along screen-up in top view
The extend-X / extend-Z / split icons share the cursor's projected X
on the wall axis, separated only by world Z (floor / cursor / wall
top). World Z collapses to a single screen point in plan view, so
every icon piled onto extend-X's hit target and only the topmost was
clickable.

Two refinements ported from gizmos-8088:

* When ``tool.Blender.is_view_top_down(context)`` reports the camera
  is near plan-view, swap world-Z stacking for screen-up stacking:
  anchor all icons at the floor world position and offset each by
  ``index * CURSOR_STACK_OFFSET`` along ``tool.Blender.get_screen_up_world(context)``.
  Each icon lands in its own screen-space slot regardless of view
  rotation.
* In the same top-down branch, drop ``extend_z_gizmo`` entirely. A
  vertical-intent gizmo has no readable cue when looking down +Z —
  clicking it would mutate the wall in a direction the user can't
  see change.
* Bonus: split's local Z now goes through
  ``core.extrusion_depth_from_vertical_height(props.height, props.x_angle)``
  so the icon lands on the slanted top edge of sloped walls (x_angle
  != 0) instead of the vertical-height target the wall isn't at.

All three helpers (``is_view_top_down``, ``get_screen_up_world``,
``extrusion_depth_from_vertical_height``) already on HEAD from PR2/PR3.
Non-top views unchanged — same world-Z stacking + cascading bumps as
before.

Generated with the assistance of an AI coding tool.
2026-06-02 19:22:38 +02:00
Gorgious56 44f5ee028f Port WallGizmoPreviewDecorator from gizmos-8088
Hover-gated viewport preview lines that show where a wall-join /
extend / split operator would land before the user clicks. Four
preview paths, each gated on a specific icon's ``is_highlight`` state:

* **Join intersection** — two LAYER2 walls selected in the ``intersect``
  state (non-joined, non-collinear, non-parallel). Draws four lines:
  each wall's axis at both base and top Z, extending from the wall's
  nearer endpoint to the projected XY intersection. The pair of lines
  per wall communicates the full plane the join welds at, not just
  the floor edge.
* **Cursor extend** — single LAYER2 wall, hover on ``extend_x_gizmo``.
  One line from the wall's nearer X endpoint to the cursor's projected
  X on the wall axis.
* **Cursor extend-Z** — hover on ``extend_z_gizmo``. Vertical line at
  the cursor's projected X from wall base to cursor Z (the new total
  height).
* **Cursor split** — hover on ``split_gizmo``. Vertical line at the
  cursor's projected X from wall base to wall top — the cut plane.
  Warning-red colour matches the icon's destructive-action signal.

Hover colour rules for the join preview:

* **Join or Fillet hover** → all four lines highlight in
  ``decorator_color_selected``. Both icons commit a symmetric corner
  meet, so every line is part of the operation.
* **Extend-to-Wall hover** → only the non-active wall's two lines
  (base + top) highlight. The default-direction extend operator
  moves the non-active wall into the active one's axis; only that
  wall's preview should signal motion.
* No hover → all four lines in ``decorations_colour``.

Three coordinated changes:

* ``bim/module/model/wall.py`` gains the ``_classify_wall_join_state``
  wrapper over ``core.classify_wall_join_state`` (feeds the
  ``_are_walls_joined`` flag the core helper expects) AND a
  ``_active_instances`` per-region weakref ClassVar on
  ``GizmoWallJoinIntersection`` populated in ``setup()``. Without the
  weakref registration, the decorator's ``_lookup_active_instance``
  call returns None every frame and the hover gates silently
  evaluate False — the symptom would be preview lines that never
  switch colour. Both pieces ported from gizmos-8088.
* ``bim/module/model/decorator.py`` gains
  ``WallGizmoPreviewDecorator`` (~280 LOC across the four preview
  paths + shared helpers ``_stroke`` /
  ``_active_layer2_wall_for_gizmo_preview`` /
  ``_join_group_hover_state`` / ``_extended_wall_index``). All
  cross-file dependencies (``core.classify_wall_join_state``,
  ``core.wall_join_preview_lines``, ``_stroke_lines_alpha``,
  ``_cursor_icon_hovered``, ``_lookup_active_instance``,
  ``tool.Parametric.is_path_connectable_wall``,
  ``_wall_axis_world_segment_from_geom``) already on HEAD.
* ``bim/handler.py`` wires ``WallGizmoPreviewDecorator.install()`` /
  ``.uninstall()`` alongside the other always-on preview decorators.
  The decorator self-polls every frame; cost is one selection-count
  check + one ``is_highlight`` read when no eligible state is active.

Verified: headless smoke green, ruff + black clean. Live testing
confirms the four preview paths fire correctly when hovering each
icon.

Generated with the assistance of an AI coding tool.
2026-06-02 17:30:13 +02:00
Gorgious56 ed7b2fc233 Stack wall-join trio along screen-up + L/T glyphs
GizmoWallJoinIntersection used to place its icons at state-specific
world points: join at floor Z, extend-to-wall at the active wall's
top Z, fillet stacked screen-up above join. Same XY at different Z
collapses to a single screen pixel in plan / top view, so two icons
became one hit target — invisible from above.

* position_gizmos now always-stacks along screen-up at a wall-top
  anchor in both the joined (unjoin + fillet) and the intersecting
  (extend + join + fillet) states. Order bottom-up is
  extend / L / fillet. Collinear-merge keeps its single boundary
  icon (no stack needed).
* New _stack_anchor_z picks the active wall's top Z (or the taller
  of the two on mid-selection-transition frames). New _stack_at
  lays a tuple of icons along screen-up at the resolved anchor.
* Glyph swap: join_icon -> VIEW3D_GT_wall_corner (L), extend_to_wall_icon
  -> VIEW3D_GT_wall_tee (T). Both classes already existed in
  bim/module/drawing/gizmos.py from an earlier commit; only the
  setup() bl_idname strings changed. The previous arrow-merge /
  arrow-extend pair read as the same direction once stacked.

Forward-compat AST contracts in test_wall_gizmos_forward_compat.py
pin the new invariants: the L and T bl_idnames must appear in
setup(), and position_gizmos must route through _stack_at so a
regression that reintroduces a direct billboarded_at write for any
state-specific icon fails CI before it flattens the stack again.

Also folds in a one-line typo fix in core/spatial.py:
assign_container's per-element can_contain check iterated `e` but
predicate-tested `root_element` (the outer for-loop variable), so
every element in the comprehension was tested against the same
container/element pair. Switch the argument to `e`.

Generated with the assistance of an AI coding tool.
2026-06-02 15:35:31 +02:00
Gorgious56 2c18155d98 Merge ifcopenshell/v0.8.0 into parametric-framework-pt2
Bring in 13 commits from upstream v0.8.0 (tip f158ae737):

- Add regenerate_wall_to_underside operator + has_underside_connection
  Model interface (closes #7943)
- Extend/regenerate walls to multiple undersides
- Fix duplicate booleans in extend_walls_to_underside
- Fix extend_walls_to_underside ridge artifact
- Regenerate connected walls when recalculating a slab
- Fix validate_type corruption; remove debug prints
- Lazy BVH tree construction in SnapObj + early-terminate solid raycasts
  in non-xray mode + optimize 2D projection in ray_cast_by_proximity_2d
- Fix crash in update_bim_tool_props when selected type isn't a valid
  ifc_class
- Fix assign_container in spatial.py (#8079)
- Fix sign of temporary offset restore in sweep_along_curve

Auto-merge resolved all overlap files cleanly:
- bim/handler.py: work branch's update_bim_tool_props refactor and
  upstream's try/except hardening converged on identical try/except
  around props.ifc_class assignment (no net change).
- bim/module/model/__init__.py: upstream's wall.RegenerateWallToUnderside
  entry and work branch's roof gizmo entries occupy disjoint sections.
- bim/module/model/wall.py: upstream's RegenerateWallToUnderside operator
  and work branch's GizmoWallEdition/IconSlot refactors occupy disjoint
  sections.
- core/tool.py: upstream's four new Model stubs and work branch's
  Root.has_material_styles stub occupy different classes.

Partly generated with the assistance of an AI coding tool.
2026-06-02 13:31:53 +02:00
Dion Moult 37aa68e66c ifcopenshell plug-in loader: stable anchor + bundle-aware fallbacks
# What

Two upstream-shaped fixes to ifcopenshell's runtime plug-in discovery
so the schema/serializer plug-ins are findable in deployment layouts
other than a flat \`<prefix>/lib/\` (specifically: macOS .app bundles).

## (1) Stable anchor variable instead of a function pointer

\`schema_plugin_directory()\` used \`&load_schema_plugins\` as the
anchor whose containing module \`dladdr\` is asked to resolve. Function
addresses are not reliably equal to a single canonical location across
toolchains — on macOS arm64 with BonsaiViewer.app, \`&load_schema_plugins\`
took the address of a PLT/stub inside the consumer binary rather than
the actual symbol inside \`libIfcParse.dylib\`. \`dladdr\` then dutifully
returned the consumer's path and the loader started searching
\`BonsaiViewer.app/Contents/MacOS/\` for plug-ins that were never
installed there.

A variable doesn't suffer from this — it has exactly one canonical
address inside its defining dylib. Add \`ifcopenshell_libifcparse_anchor\`
(exported via IFC_PARSE_API) and use \`&that\` instead. Standard pattern
used by Boost.DLL, GStreamer, \`_dyld_get_image_*\`, etc.

## (2) Bundle-aware fallback search paths

The primary search path is \`dirname(libIfcParse)\`. That works for
flat installs (Linux \`lib/\`, Windows \`bin/\`) where plug-ins are
siblings of libIfcParse. macOS app bundles split the layout:
\`macdeployqt\` puts non-Qt @rpath deps in \`Contents/Frameworks/\`,
Apple convention asks for \`Contents/PlugIns/\`, and some install rules
co-locate libIfcParse with the exe in \`Contents/MacOS/\`. Plug-ins
typically end up in a sibling directory, not the same one.

In \`add_search_paths_or_default\`, after registering the primary path,
also register \`<parent>/PlugIns\`, \`<parent>/Frameworks\`, and
\`<parent>/MacOS\` on Apple platforms. \`discover_exact\` short-circuits
on the first hit so duplicates and missing directories are harmless.

# What this does NOT do

The plug-in dylibs still need to actually be inside the app bundle
somewhere for these fallbacks to find them — the upstream install
rules (\`install(TARGETS …)\` in \`src/ifcparse/CMakeLists.txt\`,
\`src/serializers/CMakeLists.txt\`, etc.) put them in \`<prefix>/lib/\`
which lives outside \`BonsaiViewer.app\`. That side of the fix is a
follow-up — either an explicit bundle-aware install destination on
the plug-in targets, or an install(CODE) sweep that mirrors them
into the bundle.

# What this also un-does

Reverts the BonsaiViewer-only \`install(CODE)\` hack that was about to
copy \`ifcopenshell.*.dylib\` from \`lib/\` into
\`BonsaiViewer.app/Contents/MacOS/\` — superseded by the loader-side
fix above, which lets us put the plug-ins anywhere sane inside the
bundle without further consumer-side stitching.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 21:18:59 +10:00
Gorgious56 1e8c0b86a0 Migrate Modifier shim callers + drop the shim block
Completes the PR4/PR5 cleanup the FIXME at tool/blender.py
flagged: every is_<type> / Array.<helper> shim on
tool.Blender.Modifier delegated one-for-one to tool.Parametric /
tool.Array. Callers now reach the canonical home directly, and the
shim block — seven is_<type> classmethods plus the inner class Array
— comes out.

Renames (no semantic change):

* tool.Blender.Modifier.is_<door|railing|roof|stair|wall|window>
  → tool.Parametric.is_<x>
  13 sites across tool/loader.py, bim/import_ifc.py,
  bim/module/geometry/{data,operator}.py, bim/module/model/{door,
  railing,roof,stair,ui,wall,window}.py.

* tool.Blender.Modifier.Array.<helper> → tool.Array.<helper>
  4 sites across tool/root.py, bim/import_ifc.py,
  bim/module/geometry/operator.py.

* test_parametric_registry.py: the two getattr probes that hunt
  predicates by name now look on tool.Parametric. Docstring + the
  test function name (test_every_entry_has_modifier_predicate →
  test_every_entry_has_parametric_predicate) follow the move.

Kept on tool.Blender.Modifier (non-shim, no equivalent on
tool.Parametric): try_applying_edit_mode,
try_canceling_editing_modifier_parameters_or_path,
is_eligible_for_<x>_modifier (×5), is_array_child, is_slab.

Verified: 109 model-lane tests + 8 parametric-registry tests pass
(the one pre-existing failure in test_wall_header_refresh.py is
unrelated — it patches handler.update_bim_tool_props which has been
renamed). git grep for tool\.Blender\.Modifier\.(is_<type>|Array\.)
returns empty. black + ruff clean on every touched file.

Generated with the assistance of an AI coding tool.
2026-06-02 12:59:40 +02:00
Gorgious56 ac11044261 Add GizmoRoofEdition + fix low-slope normals + cancel restore
Ports roof parametric edit gizmo group from gizmos-8088 and folds in
three roof-mesh bug fixes surfaced during live testing.

Port:

* ``CycleRoofGenerationMethod`` operator (bim.cycle_roof_generation_method)
  cycles props.generation_method between "HEIGHT" and "ANGLE". Shift+click
  cycles in reverse via the ``CycleTypeMixin`` contract.
* ``GizmoRoofEdition`` gizmo group: 3 dimension gizmos for height
  (visible in HEIGHT mode) / slope angle with tan/atan2 rise round-trip
  + degree formatter (ANGLE mode) / roof_thickness. All three handles
  anchor at the object's local origin and separate visually via their
  declared axes (height/slope +Z, thickness -Z) — height + slope are
  mutually exclusive via ``visibility_condition`` so they never paint
  at the same time. Anchoring at the origin sidesteps the first-click
  default-identity-matrix symptom that footprint-derived anchoring
  would have hit on a stale ``RoofData`` cache.
* Lifecycle factory swap: explicit ``EnableEditingRoof / CancelEditingRoof
  / FinishEditingRoof`` classes replaced by ``tool.Parametric.build_edit_lifecycle("roof", _RoofEditMixin, ...)``.
  Same bl_idnames out, no external caller changes.
* Registration: ``CycleRoofGenerationMethod`` + ``GizmoRoofEdition``
  added to ``bim/module/model/__init__.py`` classes tuple.
* Tests: ``test_roof_gizmos.py`` covering slope round-trip, visibility
  gates, cycle operator metadata, and origin-anchored positioning.

Bug fixes:

* ``generate_hipped_roof_bmesh`` flipped the bottom slab face's normal
  at low slope angles. The kernel's outward-inference becomes
  ambiguous on near-flat geometry once ``remove_doubles`` and
  internal-face deletion run, and the early ``recalc_face_normals``
  pass at line 389 ran BEFORE the topology was final. A second pass
  on the final closed mesh fixes the eave plane (now reliably points
  down regardless of slope).
* ``bpypolyskel.polygonize`` can emit a face whose vertex list
  contains the same index twice on certain footprint/slope
  combinations (a straight-skeleton ridge collapse). ``bm.faces.new``
  rejects those with ``found the same (BMVert) used multiple times``,
  aborting the whole rebuild. Filter the degenerate faces out so the
  rest of the roof renders.
* ``_RoofEditMixin._restore_viewport_after_cancel`` now rebuilds the
  bmesh from the just-restored draft via ``update_roof_modifier_bmesh``.
  The hook was abstract on ``PathPreservingEditMixin`` and raised
  ``NotImplementedError`` on cancel-after-edit, leaving the user
  stranded.

Also folds in a parallel ``tool/loader.py`` swap from
``tool.Blender.Modifier.is_railing`` to ``tool.Parametric.is_railing``
(consistent with the rest of the loader using ``tool.Parametric.*``).

Verified: headless smoke green, test_parametric_registry.py 8/8,
test_roof_gizmos.py 15/15. ruff + black clean on the touched files.

Generated with the assistance of an AI coding tool.
2026-06-02 12:20:52 +02:00
Gorgious56 ab9152e32d Fix fillet preview crash + surface openings on fillet walls
Three wall-gizmo fixes:

* GizmoWallFilletPreview crashed on every draw_prepare after the
  DRY-colors refactor moved decoration lookups onto
  self.get_decoration_colors() — that method lives on
  BillboardingGizmoGroupMixin / BaseParametricGizmoGroup, but
  GizmoWallFilletPreview inherited only from bpy.types.GizmoGroup.
  setup() AttributeError'd silently, leaving radius_dim and friends
  unset. Add the mixin to the bases; rename _position_gizmos to
  position_gizmos so the mixin's refresh/draw_prepare dispatch lands
  correctly and drop the now-redundant overrides.

* GizmoWallAddOpening's poll gated on the strict is_wall predicate,
  which rejects fillet-corner walls (no LAYER2 usage by IFC spec).
  Switch to is_path_connectable_wall on both the active and the
  partner-exclusion checks so the add-opening icon surfaces over
  curved corners — matching every other wall-state gizmo's host gate.

* Show / hide openings was only available on LAYER2 walls because
  GizmoWallEdition's parametric edit pipeline (which carries the
  toggle) refuses fillet bodies. Add GizmoWallFilletToggleOpenings,
  a dedicated single-icon group that polls on is_fillet_corner_wall
  and reuses bim.toggle_wall_openings — the body stays untouched.

Forward-compat AST guards in test_wall_gizmos_forward_compat.py pin
both invariants: every wall GizmoGroup that calls
self.get_decoration_colors() must inherit a mixin that provides it,
and GizmoWallAddOpening.poll must keep using the looser predicate.

Generated with the assistance of an AI coding tool.
2026-06-02 11:39:12 +02:00
Gorgious56 18dc7abb06 Split update_bim_tool_props commit vs selection
tool.Parametric.refresh_post_commit was calling update_bim_tool_props
after every IFC mutation. The function does two things — refresh
read-only header values (extrusion_depth/length/x_angle) and re-target
user-intent enums (ifc_class, relating_type_id) from the active object.
Doing both on the commit path crashed on IfcAnnotation actives (the
type isn't in the bim_tool ifc_class enum) and silently overwrote the
user's "what to build next" choice on every other element.

Split the function: update_bim_tool_props remains selection-driven and
does both halves; new refresh_bim_tool_headers is header-only and is
what refresh_post_commit now calls. Behaviour on selection change is
preserved. Also ports the upstream PR #8136 try/except guard onto the
props.ifc_class write for the selection-driven path. Adds
test_handler_forward_compat.py to pin both contracts via AST.

Generated with the assistance of an AI coding tool.
2026-06-02 10:57:16 +02:00
Dion Moult 5ffae41bbd WgpuViewportWindow: stop using QSurface::MetalSurface on macOS
NSZombieEnabled-lldb on macOS revealed the actual cause of the
"OS_os_log displayLock" / segfault that has been chasing us:

    *** -[QMetalLayer displayLock]:
        message sent to deallocated instance 0xb5e234e40

Qt's QMetalLayer (its CAMetalLayer subclass installed when surfaceType
== MetalSurface) is dealloc'd while Qt's QCocoaWindow still holds an
internal reference to it. Once wgpu-native bridge-retains the layer in
its Rust surface code and re-publishes the drawable pool from
configureSurface, Qt's QMetalLayer life is implicitly handed to
wgpu-native and Qt's separate ref winds up dangling. The next Qt
expose event sends -displayLock to the dead pointer.

Earlier guesses (Qt-6.11/macOS-26 incompatibility, multi-display) were
both wrong — single-display still crashed, and the Tahoe os_log
selector-cast theory was a red herring; the real error is "deallocated
instance", revealed only by NSZombieEnabled.

# Fix

Set surfaceType to OpenGLSurface on macOS too. On macOS that still
gives us a layer-backed NSView; Qt just doesn't install QMetalLayer.
WgpuMetalSurface_mac.mm's else-branch (the one that always fires when
the existing layer isn't already a CAMetalLayer) now consistently
attaches a vanilla CAMetalLayer we fully own — wgpu-native can do
whatever it wants to the layer's lifetime without stepping on any
Qt-side bookkeeping.

We never bind a real GL context on top of OpenGLSurface — it's just
the most portable "hardware-rendering-ready surface" hint Qt has, and
it's already what Linux and Windows use.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 18:52:19 +10:00
Gorgious56 b7549f2476 Wire array panel buttons to triad lifecycle
Two bugs in BIM_PT_array:

1. The "is this layer in edit mode" predicate compared a BoolProperty
   against an int (props.is_editing == i). Python evaluates False == 0
   as True, so layer 0 always rendered the per-layer edit form even
   when no edit was active — clicking validate/cancel then dispatched
   against a phantom edit state. Switched to
   props.editing_item_index == i, which defaults to -1 and matches
   exactly one layer when an edit is active.

2. The panel's CHECKMARK and CANCEL buttons called bim.edit_array /
   bim.disable_editing_array, a parallel lifecycle that only cleared
   editing_item_index. Entering edit mode via the viewport gizmo
   (bim.enable_editing_array, the triad enter) sets is_editing=True
   and hides array children; the legacy panel exit unwound neither —
   so committing or cancelling from the panel left is_editing=True
   with children hidden, and the viewport gizmo thought the edit was
   still in progress. Re-bound both panel buttons to the canonical
   triad operators (bim.finish_editing_array /
   bim.cancel_editing_array), which _ArrayEditMixin already owns and
   which the viewport gizmo group already uses. Panel and gizmo now
   share one exit path.

The three now-unreachable operators are deleted with their
registration entries: EditArray (bim.edit_array), DisableEditingArray
(bim.disable_editing_array), and EnableEditingArrayItem
(bim.enable_editing_array_item, never called from any UI). The two
test/tool/test_model.py sites that drove bim.edit_array as a commit
step are switched to bim.finish_editing_array.

External scripts or user keymaps bound to bim.edit_array /
bim.disable_editing_array will need to update — the replacements are
bim.finish_editing_array and bim.cancel_editing_array, both taking no
parameters (the layer is read from props.editing_item_index).

Partly generated with the assistance of an AI coding tool.
2026-06-02 10:37:01 +02:00
Gorgious56 ba6cfe9c24 Fix door swing arcs + declarative SwingArcConfig
The recent per-gizmo-prefs cleanup left ``update_swing_gizmos`` with a
stale ``prefs`` reference that raised NameError mid-refresh, so the flip
arc's ``matrix_basis`` was never reassigned and the gizmo drifted to the
world origin. SINGLE_SWING_RIGHT also lacked an X-mirror on the primary
arc, so the swing extended past the door's right edge instead of
sweeping back over the panel.

Five related fixes / additions:

* Drop the leftover ``prefs.decorations_colour[:3]`` per-frame colour
  override (the setup-time ``decorator_color_special`` is the durable
  contract — there's no reason to overwrite it every refresh).
* Add X-mirror to RIGHT-hinged single-panel transforms so the arc
  sweeps back over the door rather than past the right edge.
* Treat DOUBLE_DOOR_SINGLE_SWING as a two-panel layout: 4 arcs total
  (left + right panels, each with its own Y-mirrored flip) scaled to
  ``overall_width / 2``.
* Hide all swing arcs for SLIDING_TO_LEFT / SLIDING_TO_RIGHT /
  DOUBLE_DOOR_SLIDING — sliding doors don't swing. A slide-direction
  indicator is deferred to a separate change.
* Pin ``select_bias = -1000.0`` on every arc gizmo so the big
  quarter-arc hit shapes don't steal clicks from the smaller dimension
  and edit gizmos drawn on top.

Architectural cleanup driven by the same diff: the imperative
4-create + 50-line update block is replaced by a declarative
``swing_arc_props`` list of ``SwingArcConfig`` entries (mirrors the
existing ``dimension_gizmo_props`` pattern). Setup iterates the list
and creates one (main, flip) pair per entry under
``gizmo_swing_arc_<name>`` / ``gizmo_swing_arc_<name>_flip``; update
iterates the same list and positions each pair via the lambdas. Adding
a hypothetical multi-panel variant becomes a config entry rather than
two more attribute names plus a transform branch.

``ToggleDoorSwing`` gets a ``description`` classmethod that returns
user-facing wording per ``flip_geometry`` branch so the tooltip on
hover stops reading like operator internals.

``test/bim/module/model/test_door_gizmos.py`` (new) pins the
per-door-type contract: 11 cases covering LEFT / RIGHT hinge positions,
DOUBLE_SWING parity with SINGLE_SWING, DOUBLE_DOOR 4-arc layout, the
sliding-types hide invariant, ``is_editing=False`` hide invariant,
flip-arc matrix re-assignment, and world-matrix pre-multiplication.

Verified: ``pytest test/bim/module/model/test_door_gizmos.py`` 11/11
green; combined wall + stair + door gizmo lanes 37/37 green; ruff +
black clean on the three touched files.

Generated with the assistance of an AI coding tool.
2026-06-02 10:15:09 +02:00
Dion Moult aba8ac727d wgpu: query surface capabilities before configuring present mode
Default WGPUPresentMode was a static Mailbox. That works on DX12 and
on Vulkan with most drivers, but the Metal backend in wgpu-native v29
only exposes [Fifo, Immediate], and asking for Mailbox makes
wgpuSurfaceConfigure panic from Rust:

    thread '<unnamed>' panicked at src/lib.rs:605:5:
    Error in wgpuSurfaceConfigure: Validation Error
    Caused by:
      Requested present mode Mailbox is not in the list of supported
      present modes: [Fifo, Immediate]
    fatal runtime error: failed to initiate panic, error 5, aborting

# Fix

Query wgpuSurfaceGetCapabilities and walk a preference list
(Mailbox → FifoRelaxed → Immediate → Fifo), picking the first mode
the surface actually lists. Fifo is the only spec-required mode and
will always be present, so the loop always finds something.

The env override (WGPU_PRESENT_MODE=...) still wins when set, but
also drops back to Fifo if the requested mode isn't supported on the
current backend — no panic.

# Outcomes per backend

  DX12 / Vulkan:  picks Mailbox (low input lag, our previous default).
  Metal (macOS): picks Immediate (Mailbox unavailable). On Metal the
                 CAMetalLayer presents through CoreAnimation, so the
                 compositor still vsync-aligns; Immediate is effectively
                 low-latency-with-no-tearing on macOS.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 17:47:46 +10:00
Dion Moult f66ad22f1d macOS: ship libwgpu_native.dylib into the bundle and set the rpath
BonsaiViewer.app crashed at launch on a fresh macOS arm64 mac with:

    Library not loaded: @rpath/libwgpu_native.dylib
    Referenced from: /Applications/BonsaiViewer.app/Contents/MacOS/BonsaiViewer
    Reason: no LC_RPATH's found

The exe had \`LC_LOAD_DYLIB @rpath/libwgpu_native.dylib\` (CMake baked
that in from the upstream dylib's install_name), but zero \`LC_RPATH\`
entries, so dyld had nowhere to look — and macdeployqt hadn't pulled
the dylib in either, since it sat at \`<install_root>/lib/\` rather
than inside the .app.

Two changes:

- \`src/ifcviewer-wgpu/CMakeLists.txt\`: on macOS, when
  BUILD_BONSAIVIEWER is on, install libwgpu_native.dylib straight
  into \`BonsaiViewer.app/Contents/Frameworks/\` instead of
  \`<prefix>/lib/\`. That matches the standard macOS bundle layout.

- \`src/bonsaiviewer/CMakeLists.txt\`: set
  \`INSTALL_RPATH "@executable_path/../Frameworks"\` on the
  BonsaiViewer target on Apple. That's where dyld looks at launch,
  and where the dylib now lives.

Together the @rpath load resolves at launch without depending on
macdeployqt to follow non-Qt @rpath references (it usually only
chases Qt frameworks).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 17:05:41 +10:00
Dion Moult edc598357b wgpu: default present mode to Mailbox (was Fifo)
# Why

The stage-1 default was Fifo because it is the only present mode
WebGPU spec *requires* every backend to support — conservative,
"always works", lowest power. But on DX12 Fifo maps to a DXGI
flip-discard swap chain whose default \`MaximumFrameLatency\` is 3,
which queues ~50ms of pre-rendered work between submit and display.
Even when the FPS counter shows 60 the cursor-bound interactions
(marquee, pivot, orbit) feel ~3 frames behind because they *are*.

# What

Mailbox: vsync-aligned (no tearing) with a one-frame queue
(last-frame-wins). ~16ms input→display latency. wgpu-native handles
the fallback to Fifo automatically on backends that don't implement
Mailbox (Vulkan + NVIDIA on Linux is the historical one).

# Trade-off

Mailbox uncaps the render loop: with no vsync gating, the
\`requestUpdate()\` → render → present loop spins at whatever Qt's
event loop allows (~600 Hz on an empty scene), and the GPU does
useful-but-discarded work on each redundant frame. On any non-trivial
scene the GPU is the bottleneck and the loop self-paces near the
display rate. The FPS counter measures \`render()\` invocations, not
unique frames the user actually sees — that's expected.

# Override

WGPU_PRESENT_MODE=fifo restores the old behaviour for the rare
laptop-battery / heat-conscious case. fifo_relaxed / immediate also
still work as before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 16:45:54 +10:00
Ryan Schultz f158ae7377 Fix crash in update_bim_tool_props when selected type isn't a valid ifc_class
props.ifc_class is an EnumProperty whose items list only the element/space
types present in the model. Assigning element_type.is_a() crashed with
`enum "<class>" not found` when the selected element's type wasn't a member
(e.g. a raw IfcTypeProduct, or a stale item list mid-rebuild), aborting the
post-commit refresh.

Wrap the assignment in the same try/except TypeError guard already used for
the sibling relating_type_id assignments (added in 233cc344fa).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 08:44:21 +02:00
Gorgious56 b154cadf3b Add IconSlot placeholders + stair xN tread label
Add a clickable "xN" badge to GizmoStairEdition's edit row, mirroring
the array's popup-input UX: click opens a number dialog (no more
shift+click-into-modal). Text-only — no 2x2 grid glyph.

Structural changes that enable this cleanly:

* IconSlot.placeholder=True: slots reserve an X position in the row
  without auto-creating a gizmo. Subclasses resolve the reserved X via
  _slot_x_positions()[name] to place their own dynamic gizmos. Drops
  the brittle "remember to add extra_gap_before" workaround that would
  silently rot on slot reorders.

* Array bug fix: the count badge collided with the "-" icon because
  the slot manager placed count_minus at the cycle position (X=0.87)
  where ICON_NUMBER_X also lives. Migrating the badge to a placeholder
  slot lets the manager allocate the X naturally and the "-" no longer
  overlaps. ICON_NUMBER_X constant removed.

* IntegerInputDialogMixin in parametric_lifecycle.py: extracts the
  popup-dialog plumbing shared between InputArrayCount and the new
  InputStairTreads. Subclasses declare an IntProperty + attr_name +
  props_getter; the mixin owns invoke/execute. _resolve_props helper
  factors the common obj/props/requires_editing prologue.

Tests: BIM_GT_count_label registration; IconSlot placeholder contract
(no gizmo_idname required; gizmo_attrs() returns empty); the stair
edit-row slot layout reserves the label position between tread_lock
and plus at one ICON_ARRAY_GAP each; visibility propagates from
props.is_editing.

Partly generated with the assistance of an AI coding tool.
2026-06-02 08:38:01 +02:00
Dion Moult 6b72a60d8c WgpuMetalSurface_mac: switch to <AppKit/AppKit.h> umbrella
The narrow `<AppKit/NSView.h>` header only forward-declares NSWindow,
so `view.window.backingScaleFactor` fails to compile on macOS with:

    error: property 'backingScaleFactor' cannot be found in forward
           class object 'NSWindow'

Use the AppKit umbrella header so NSWindow's interface (including
backingScaleFactor) is in scope. Same idiomatic include any non-trivial
Cocoa code reaches for; we're already linking -framework Cocoa so
there's no compile-time cost beyond a slightly heavier translation
unit (the .mm is ~30 lines anyway).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 14:25:06 +10:00
Dion Moult 21d394501f ifcviewer-wgpu: bring up macOS Metal surface (task #32)
Without this, BonsaiViewer.app launched on macOS would show a white
viewport for the same reason Windows did before d4d0c934b — the
`createSurface()` else branch fell through to "wgpu surface creation
not yet wired for this platform", `init()` returned false, and the
Qt window background was all the user saw.

Pieces:

- `WgpuMetalSurface_mac.{h,mm}`: tiny Objective-C++ bridge. Takes the
  NSView pointer that Qt's `winId()` returns on macOS, attaches a
  CAMetalLayer (using Qt's existing one when surfaceType is
  MetalSurface, attaching one ourselves as a defensive fallback),
  sets `contentsScale` from the window's backing scale factor so
  retina drawables come out at native resolution, and returns the
  layer as `void*`. The .mm keeps the Objective-C namespace pollution
  out of WgpuViewportWindow.cpp.

- `WgpuViewportWindow` ctor: `setSurfaceType(QSurface::MetalSurface)`
  on macOS so Qt backs the NSView with a CAMetalLayer at window
  creation; OpenGLSurface elsewhere as before.

- `WgpuViewportWindow::createSurface()`: new `#elif defined(Q_OS_MAC)`
  branch that fills a `WGPUSurfaceSourceMetalLayer` with the layer
  pointer from the bridge and hands it to `wgpuInstanceCreateSurface`.

- `CMakeLists.txt`: `enable_language(OBJCXX)` + the .mm file added to
  the source list on Apple, and links `-framework Cocoa` (NSView) +
  `-framework QuartzCore` (CAMetalLayer).

`winId()` on macOS returns the backing NSView*, not the NSWindow* —
that's the layer-bearing host wgpu-native expects.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 13:45:42 +10:00
Dion Moult d4d0c934b1 ifcviewer-wgpu: wire Windows HWND surface creation
Without a Windows branch in `WgpuViewportWindow::createSurface()` we
were falling through to the `qWarning() << "wgpu surface creation not
yet wired for this platform"` else clause on Windows runs, causing
`init() -> createSurface()` to return false and the viewport to render
nothing (the user sees the Qt window's background fill — a white
viewport — and the log says "wgpu init failed; viewport will not
render in DebugView").

Add a `#elif defined(Q_OS_WIN)` branch that fills a
`WGPUSurfaceSourceWindowsHWND` chained-struct from
`GetModuleHandleW(nullptr)` (HINSTANCE) and `winId()` (HWND, as a Win32
window handle on the Qt Windows platform plugin), then passes it as
`surface_desc.nextInChain` to `wgpuInstanceCreateSurface`.

`<windows.h>` is pulled in inside the gated block with NOMINMAX and
WIN32_LEAN_AND_MEAN defined first so the preprocessor pollution
(`min`, `max`, etc.) doesn't leak into Eigen / `std::min`,`std::max`
elsewhere in the TU.

Note on the unrelated DXC log line the user also sees:

    [wgpu err] DxcCreateInstance failed:
    No such interface supported (0x80004002)

That is wgpu-native's DX12 backend probing for a modern
`dxcompiler.dll`. `E_NOINTERFACE` means a *too-old* dxcompiler.dll was
found on the system DLL search path (typical: a stale copy in
System32 / Visual Studio install). wgpu-native then falls back to its
Vulkan backend, so this log line is recoverable on its own — the
fatal failure was the missing surface branch above. If we hit shader
compilation issues after this lands, we can ship a known-good
dxcompiler.dll + dxil.dll alongside wgpu_native.dll separately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 13:32:01 +10:00
Bruno Perdigão 5dde402f8b Optimize 2D projection in ray_cast_by_proximity_2d 2026-06-01 22:27:58 -03:00
Bruno Perdigão 1daee04d9c Early-terminate solid raycasts in non-xray mode 2026-06-01 22:18:13 -03:00
Dion Moult 1b920e502f ifcviewer-wgpu: pick the Windows ARM64 wgpu-native archive when targeting ARM64
The Windows branch of the wgpu-native FetchContent block was
hardcoding the x86_64 archive name regardless of host arch — the
macOS and Linux branches already switch on
\`CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64"\`, but Windows
didn't get the same treatment because the wgpu work was done on
x86_64 hosts.

Result on \`windows-11-arm\`: CMake downloaded
\`wgpu-windows-x86_64-msvc-release.zip\`, IfcViewerWgpu linked against
the x86_64 import library, and the final link of
IfcViewerWgpuMinimal/BonsaiViewer emitted ~60 unresolved \`wgpu*\`
externs because the import-lib symbols are x86_64-only.

Upstream wgpu-native v29.0.0.0 already publishes
\`wgpu-windows-aarch64-msvc-release.zip\` — switching on
\`CMAKE_SYSTEM_PROCESSOR\` so the right archive gets fetched is
sufficient.

Also restores the ARM64 row in \`.github/workflows/build_win.yml\`
that the prior commit dropped (the comment there was wrong; upstream
does ship the binary, our CMake just wasn't asking for it).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 10:13:01 +10:00
Dion Moult 6fa55b7c25 build_osx: zip and upload .app bundles to S3
The existing "Package .zip archives" step only sweeps
\`\$install_root/bin/\` for plain executable files (via \`find -type f
-perm /111\`). That captures \`IfcConvert\` and \`IfcGeomServer\` but
misses macOS app bundles entirely:

  - BonsaiViewer.app installs at \`\$install_root/BonsaiViewer.app\`
    (BUNDLE DESTINATION ".") — not under bin/, and it's a directory,
    not a file.

So the prior bonsai macOS CI run got a green tick but the
ifcopenshell-builds S3 bucket only ended up with IfcConvert +
IfcGeomServer + the python wheel — no BonsaiViewer.

Add a second packaging pass that finds \`*.app\` directories at the
install-prefix root and zips each one as-is. macdeployqt has already
embedded the Qt frameworks inside the bundle during install/strip, so
no extra dependency staging is needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 09:49:04 +10:00
Bruno Perdigão 0d7c378db5 Lazy BVH tree construction in SnapObj 2026-06-01 20:47:27 -03:00
Dion Moult b0ef47819f ci: IFCOS_BUILD_PYTHON_WRAPPER env-gate (off for bonsai macOS CI)
# Why this exists

The bonsai macOS CI (`build_osx.yml`, arm64) currently fails the
`IfcOpenShell-Python` smoke test with:

    ImportError: dlopen(.../_ifcopenshell_wrapper.cpython-311-darwin.so,
                       0x0002):
      Library not loaded: @rpath/ifcopenshell.document.rdb.dylib
      Reason: tried: '$ORIGIN/ifcopenshell.document.rdb.dylib'
                     (no such file)

`_ifcopenshell_wrapper.cpython-311-darwin.so` has a hard `LC_LOAD_DYLIB`
of `@rpath/ifcopenshell.document.rdb.dylib` and its only `LC_RPATH` is
`$ORIGIN` (= `site-packages/ifcopenshell/`). The plug-in dylib is not
present at that path on macOS, so the wrapper fails to load and the
build smoke test (`build-all.py: compile_python_wrapper`) errors out.

BonsaiViewer.app builds, installs, and macdeployqt-deploys cleanly
before this point — the failure is downstream and unrelated to wgpu,
BonsaiViewer, or anything else on this branch.

# Where the regression came from

Two commits on the branch line that became `ifcviewer-wgpu`:

  b599ee10 "More work on isolating into plug-ins"   (2026-04-18, Thomas Krijnen)
  b022ca7e7 "Some plug-in work"                     (2026-04-21, Thomas Krijnen)

`b599ee10` added a hard link dep:

    target_link_libraries(ifcopenshell_wrapper PRIVATE document_serializer_rdb)

which bakes `@rpath/ifcopenshell.document.rdb.dylib` into the wrapper's
`LC_LOAD_DYLIB`. `b022ca7e7` added a `if(CREATE_BUNDLE) ...
install(TARGETS ${_ifcopenshell_python_runtime_targets}
LIBRARY DESTINATION "${python_package_dir}/ifcopenshell" ...)` block
that was *intended* to satisfy that link dep by copying plug-ins next
to the wrapper. On macOS arm64 the install rule does not actually
deposit `ifcopenshell.document.rdb.dylib` into
`site-packages/ifcopenshell/`, so the runtime dlopen fails.

# Why 227d85d worked

`227d85d` (2026-05-15) is on the `v0.8.0` line, not on the
`datamodel-v1.0 -> ifcviewer -> ifcviewer-wgpu` line. The merge-base
of `227d85d` and `ifcviewer-wgpu` is `e6258ab4` (2026-04-13). Both
b599ee10 and b022ca7e7 live on the wgpu side of that fork and are not
ancestors of `227d85d`:

    $ git merge-base --is-ancestor b599ee10 227d85d
    [exit 1 — NOT an ancestor]
    $ git merge-base --is-ancestor b599ee10 v0.8.0
    [exit 1 — NOT an ancestor]

So the macOS Python wheel built fine on `v0.8.0` because that branch
never had the plug-in refactor; it has been broken on our branch line
since 2026-04-21. Nobody noticed because nobody had been firing
`build_osx.yml` against this branch line until this week's bonsai CI
work.

# What this commit does

Adds an `IFCOS_BUILD_PYTHON_WRAPPER` env var to `nix/build-all.py`.
Defaulting to `on` preserves existing behaviour everywhere; setting
it to `off` (or `0`/`false`/`no`) drops `IfcOpenShell-Python` from
the target set so `build-all.py` skips the wrapper build + smoke
test entirely.

`build_osx.yml` sets `IFCOS_BUILD_PYTHON_WRAPPER=off` so the bonsai
macOS CI can complete and upload `BonsaiViewer.app` while the plug-in
install rule is broken.

# What Thomas should do

Once the install rule in `src/ifcwrap/CMakeLists.txt` (the
`if(CREATE_BUNDLE) ... install(TARGETS ${_ifcopenshell_python_runtime_targets}
LIBRARY DESTINATION "${python_package_dir}/ifcopenshell" ...)` block,
added in b022ca7e7) is fixed to actually drop
`ifcopenshell.document.rdb.dylib` next to the wrapper in
site-packages on macOS — this commit can be reverted in its entirety:
the env-gate in `build-all.py` AND the `IFCOS_BUILD_PYTHON_WRAPPER=off`
in `build_osx.yml`. The bonsai macOS workflow will then build the
Python wrapper too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 09:06:28 +10:00
Dion Moult 938dda80d0 ci: GCC 11 portability + BUNDLE DESTINATION "." on macOS
Linux (Rocky manylinux, GCC 11):

- WgpuAreaMeasurement.h: include <cstddef> directly. GCC 11 does not
  transitively pull `size_t` through <vector>, so triangleCount()'s
  return type fails to parse.

- WgpuViewportWindow.cpp:meshLocalToGlobal: use static_cast<double>(...)
  instead of double(mesh_local[N]) when constructing the Eigen::Vector4d.
  The latter triggers GCC 11's most-vexing-parse: it reads
  `Vector4d local(double(mesh_local[0]), double(mesh_local[1]), ...)`
  as a function declaration of `local` taking parameters
  `double mesh_local[0]` etc., colliding with the outer `mesh_local`
  parameter and failing with "redefinition of double* mesh_local".

macOS arm64:

- IfcViewerWgpuMinimal + BonsaiViewer install rules: change
  `BUNDLE DESTINATION bin` → `BUNDLE DESTINATION .`. Qt's deploy
  generator emits `macdeployqt <Target>.app` with no path prefix, which
  only resolves when the bundle sits at the install-prefix root.
  `BUNDLE DESTINATION bin` put it at `<prefix>/bin/Target.app` and the
  install/strip step failed with "Could not find app bundle".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 07:53:57 +10:00
Gorgious56 1ba9341201 Drop per-gizmo preferences + fix dynamic-wall face normals + DRY colors
Three related cleanups in one pass:

* **Per-gizmo preferences removed.** The ``visibility_pref`` field on
  IconSlot, the ``prefs.gizmos.<feature>.<icon>`` PropertyGroups, and
  the dispatcher that surfaced them in the addon preferences UI are
  all gone. ``update_gizmo_visibility`` loses its ``pref_enabled``
  parameter — visibility is now driven purely by editing state and
  modal gating. bim/ui.py drops ~257 lines of dead PropertyGroup
  definitions; bim/__init__.py and tool/parametric.py shed their
  matching wiring; door / wall slot declarations stop referencing
  the now-nonexistent prefs.

* **Dynamic-wall face normals fixed.** ``regenerate_wall_mesh_from_props``
  in wall.py now calls ``bmesh.ops.recalc_face_normals`` before writing
  the mesh. Without it, walls regenerated from the parametric edit
  draft could ship with inward-facing normals on some faces, which
  rendered as visual holes under any backface-cull or normal-aware
  shading. ``test/bim/module/model/test_wall_preview_mesh.py`` pins
  the invariant (every face's normal points away from the wall centre).

* **Color constants DRY.** ``COLOR_RED`` / ``COLOR_GREEN`` /
  ``COLOR_BLUE`` / ``COLOR_NEUTRAL`` now live at module scope in
  gizmos.py; the BaseParametricGizmoGroup class attributes alias the
  same tuples so ``self.COLOR_GREEN`` keeps working. IconSlot
  declarations in stair.py (plus / minus) and array.py (count_minus /
  count_plus / delete) now reference the named constants instead of
  duplicating the RGB tuples inline.

Verified: headless smoke green at 1267 BIM_OT_ classes,
test_parametric_registry.py 8/8, wall lane 31/31 (includes the new
preview-mesh test). ruff + black clean on the touched files.

Generated with the assistance of an AI coding tool.
2026-06-01 18:32:54 +02:00
Gorgious56 f0aec7b38e Highlight partner wall on link-toggle hover
Hovering a wall-junction link-toggle icon today only swaps the icon
shape — the user doesn't see which wall the click will disconnect from
until after they click. ATPATH (T-junction) configurations especially
make the partner ambiguous when multiple connections sit close together.

On hover, paint a wireframe bbox around the partner wall using the same
shader, constants and color the array module already established for
its layer-children highlight (POLYLINE_UNIFORM_COLOR, decorator_color_special,
line width 1.8, alpha 0.8). The line-width / alpha constants in decorator.py
are renamed from _ARRAY_LAYER_BBOX_LINE_* to _BBOX_HIGHLIGHT_LINE_* and
shared between draw_array_layer_children_bbox and the new
draw_wall_partner_bbox so the two highlights stay in lockstep.

The trigger lives in a new GizmoWallLinkToggle subclass in wall.py
which keeps the base gizmos.GizmoLinkToggle generic (per the
generic-naming convention for shared widgets). The subclass's draw()
calls super().draw(context) then on self.is_highlight outlines its
partner_obj via the shared decorator helper. Same trigger pattern as
GizmoArrayLayerIndicator.

Blender's Gizmo API exposes target_set_operator but no symmetric
getter, so the partner reference can't be read back from the bound
operator handle. Instead GizmoWallUnjoinSingle.position_gizmos
mirrors the resolved partner_obj onto each visible icon every frame
next to the existing other_wall_guid write — the icon's draw() reads
from its own __slots__-declared attribute.

A forward-compat AST test pins the contract: GizmoWallLinkToggle.draw
must reference is_highlight and call draw_wall_partner_bbox. Catches
the regression where someone tidies the draw() override into super()
or replaces the shared helper with an ad-hoc draw call.

Generated with the assistance of an AI coding tool.
2026-06-01 16:40:18 +02:00
Gorgious56 4cf34b69d2 Replace hardcoded icon-X constants with IconSlot layout manager
The parametric edit toolbar row used to assign each feature icon its
own ICON_<NAME>_X constant, with a separate FEATURE_ICON_MAX_X override
each subclass had to bump whenever a new icon was added. Forgetting the
bump silently collided icons — wall's rotate icon and the array button
both landed at X=1.24 in edit mode.

The new IconSlot dataclass + feature_slots tuple replace the
constants-and-override pattern with order-driven positioning: the
layout manager assigns each slot an X from its tuple index plus a
uniform ICON_ARRAY_GAP. Adding an icon is now a one-line append; the
"forget to bump" failure mode is structurally impossible.

Slot capabilities cover every existing icon-row shape:
* Single icon (wall rotate, array delete).
* N-variant slots — N gizmos at the same X with one visible per frame
  via a subclass picker (stair tread-lock open/closed, wall baseline
  exterior/center/interior). Pair becomes the N=2 case; triplet the
  N=3 case. Variant idnames can be authored either as a tuple of
  explicit names or as a string prefix that auto-suffixes _<variant>.
* Visibility prefs gate slot rendering without reflowing the row —
  hidden slots still consume their X position.
* Extra per-slot gap before for visual separation (array's delete
  trails the routine controls by an extra 0.2 m).
* Operator props forwarded to target_set_operator so adjusters
  (+/-, increment) and generic toggles (property_name=...) work.

When the cycle slot is unused, feature slots collapse into the cycle
position so the row stays tight — that's how wall's baseline triplet
sits at X=0.87 without a gap before it.

Three subclasses migrate to the new system:
* wall.py — rotate icon + baseline triplet variants. Drops
  ICON_ROTATE_X, _BASELINE_GIZMO_ATTRS, the manual triplet creation
  loop, and the matching positioning block in _update_icon_row_extras
  (it now just picks variant visibility).
* stair.py — tread_lock pair (open/closed) + plus + minus.
  _update_editing_icon_positions reads slot X via _slot_x_positions
  instead of three hardcoded constants. Also fixes the standalone
  total_length_lock gizmo, which was broken since PR4 split
  VIEW3D_GT_lock into open/closed pair (caller wasn't updated).
* array.py — count_minus + count_plus + method + delete (with
  extra_gap_before=0.20 to separate the destructive action).
  Drops the manual edit-row positioning loop entirely; the base
  loop handles it. GizmoArrayChild now inherits BillboardingGizmoGroupMixin
  and uses the shared setup_icon_gizmo helper, dropping its
  duplicated _make_icon wrapper.

Two helpers added on BillboardingGizmoGroupMixin to fold the duplicated
prefs/color preamble that appeared at the top of six wall gizmo setups
plus the array-child setup:
* get_decoration_colors() — (decorations_colour, decorator_color_selected),
  the active-state pair.
* get_unselected_decoration_colors() — (decorator_color_unselected,
  decorator_color_selected) for gizmos surfaced on already-selected
  geometry that should not pull focus.

Verified: headless smoke green at 1267 BIM_OT_ classes,
test_parametric_registry.py 8/8 pass, wall lane 29/29 pass,
model lane unchanged at 135 pass + 7 pre-existing v0.8.0 failures
(no regressions). ruff + black clean.

Generated with the assistance of an AI coding tool.
2026-06-01 16:19:42 +02:00
Dion Moult 45e9f763c8 ci: fix bonsai cross-platform build on Linux, macOS arm64, Windows x64
Bundles four portability fixes uncovered by manually firing the platform
workflows against this branch:

- WgpuOverlayRenderer.cpp: GCC 11 (Rocky manylinux runner) does not parse
  a multi-line raw string inside `#define`. Converted
  THICK_LINE_HELPERS_WGSL from a `#define` to a `static const char*` and
  switched AXIS_WGSL / SECTION_WGSL / MARQUEE_WGSL to `std::string` so
  they can concatenate at static-init time. Three call sites now pass
  `.c_str()` to svFromCStr.

- bonsaiviewer/CMakeLists.txt: added BUNDLE DESTINATION to the install
  rule (same fix already applied to IfcViewerWgpuMinimal). MACOSX_BUNDLE
  targets fail at configure on macOS without it even when nobody runs
  `make install`.

- build_osx.yml: dropped the x64 (Intel cross-compile) matrix row. The
  runner is arm64 so `brew --prefix qt` returns the arm64 prefix; we'd
  need a separate x86_64 Qt install under /usr/local to cross-build
  BonsaiViewer. Revisit if Intel-Mac demand resurfaces.

- build_win.yml: dropped the ARM64 matrix row. wgpu-native does not ship
  a Windows-ARM64 binary, so IfcViewerWgpu's link step fails with ~60
  unresolved wgpu* externs. Re-enable when upstream publishes that
  target.

Cherry-pick this commit to v0.8.0 so the workflow_dispatch buttons see
the dropped rows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 23:13:59 +10:00
Gorgious56 44723e83b6 Add link-toggle hover gizmo for wall junctions
The previous single-wall unjoin gizmo used a bracket-pair icon
(VIEW3D_GT_unjoin) that reads as "unjoin" only after you know what
it is, with no clear "linked" inverse — closing the brackets to
suggest the connected state collapses to a hollow square that
doesn't read as a link at all.

Add GizmoLinkToggle (VIEW3D_GT_link_toggle): two filled dots joined
by a horizontal connector in the default state. On hover the two
halves shear vertically apart — left dot+stub slip down as a unit,
right dot+stub slip up — with a horizontal gap at the centre,
signalling that a click will sever the underlying connection. The
glyph lives next to the generic icon classes (GizmoLockOpen/Closed,
GizmoArc) so any path / link / pair-of-connected-items context can
reuse it; it isn't wall-specific despite the first caller.

The class keeps its own per-state GPUBatch cache so the shape swap
on hover doesn't allocate per frame. The hit-shape is sourced from
the broken form (the larger bbox of the two states) so the cursor
doesn't lose hover at the offset dots' outer edges and flicker
between states.

GizmoWallUnjoinSingle.setup() now requests VIEW3D_GT_link_toggle.
The operator binding (bim.unjoin_wall_path_connection), the
POOL_SIZE, and the per-frame partner-GUID write are unchanged.

Generated with the assistance of an AI coding tool.
2026-06-01 14:51:31 +02:00
Gorgious56 2272c35e9b Fix spurious X/Y rotation on fillet corner wall
When the two source walls were placed at different elevations, the
fillet corner wall ended up with sub-degree X and Y Euler rotations
even though both source walls had only a Z rotation.

Cause: _apply_fillet_corner_geometry derived the corner's local X
axis from `chord = tangent_b - tangent_a` (a 3D vector). With walls
at different Z, `chord.z` was non-zero, so `x_dir = chord.normalized()`
inherited that Z component. The Z axis was already hardcoded to world
Z, so x_dir and z_dir were no longer orthogonal — the resulting
matrix_world was non-orthonormal, and Blender's Euler decomposition
surfaced the skew as the visible X/Y rotation drift.

Project the chord to the XY plane before normalising so x_dir is
strictly XY-aligned and orthogonal to z_dir. The corner wall is now
placed at wall A's elevation with a pure Z rotation, which matches
the user's expectation when both inputs are Z-aligned regardless of
their relative elevation.

Generated with the assistance of an AI coding tool.
2026-06-01 14:49:33 +02:00
falken10vdl 1c128a2d6a Add has_underside_connection method to Model class and update wall regeneration logic 2026-06-01 07:44:49 -05:00
Ryan Schultz 36372627db Fix validate_type corruption; remove debug prints
When validate_type selected a preferred_item from remaining_items
(e.g. the sole IfcBooleanResult in a representation), it left that
item in the list. The subsequent Items filter removed every item,
leaving Items=[] and causing guess_type to return
"MappedRepresentation" — silently corrupting the representation.

Also removes temporary debug print statements added during
investigation of the wall-to-slab extension workflow.

Generated with the assistance of an AI coding tool.
2026-06-01 07:44:49 -05:00
Ryan Schultz a9b6f02f02 Fix duplicate booleans in extend_walls_to_underside
Re-running the operator on the same wall/slab pair created
additional IfcPolygonalFaceSet booleans each time. Now each
wall's existing booleans are removed before re-clipping, and
previously connected slabs are merged with the new selection
so no earlier clips are silently discarded.

Generated with the assistance of an AI coding tool.
2026-06-01 07:44:49 -05:00
Ryan Schultz 9e7d97e298 Regenerate connected walls when recalculating a slab
When Shift+G is pressed on a LAYER3 element, any LAYER2 walls
connected via IfcRelConnectsElements(TOP) are now re-clipped
to the slab's updated geometry after recalculate_slab runs.

Generated with the assistance of an AI coding tool.
2026-06-01 07:44:49 -05:00
Ryan Schultz 187b8e7167 Add extend/regenerate walls to multiple undersides
extend_walls_to_underside now accepts multiple slab/roof
objects in a single operation — all selected non-LAYER2 IFC
elements are treated as clip targets, all LAYER2 elements as
walls. Placement sync is done once upfront; each wall is then
clipped against every selected slab before reloading.

Also adds bim.regenerate_wall_to_underside (Shift+G): after
moving a slab, re-clips connected walls using the existing
IfcRelConnectsElements(TOP) relationship. Old booleans are
removed via remove_representation_item before re-clipping.

Generated with the assistance of an AI coding tool.
2026-06-01 07:44:49 -05:00
Ryan Schultz 08e33fe572 Closes #7943: Add regenerate_wall_to_underside operator
When extend_walls_to_underside is applied to a wall and the
roof/slab is later moved, pressing Shift+G now re-clips the
wall to the slab's new position.

The IFC relationship created by connect_wall_to_slab
(IfcRelConnectsElements, Description="TOP") is used to look
up which slabs a wall is clipped to. On regeneration, the
existing manual booleans (IfcPolygonalFaceSet operands) are
cleanly removed via remove_representation_item, then
clip_wall_to_slab is re-applied for each connected slab.

Shift+G on a LAYER2 wall that has a TOP connection now calls
bim.regenerate_wall_to_underside; walls without a connection
continue to call bim.recalculate_wall as before.

Generated with the assistance of an AI coding tool.
2026-06-01 07:44:49 -05:00
Ryan Schultz e142b9d7b4 Fix extend_walls_to_underside ridge artifact
When the operator was called twice on the same wall for a
ridge roof, the two IfcPolygonalFaceSet clip solids shared
an exact ridge edge (kissing-solid). OCCT produced spurious
extra vertices at the coincident boundary.

Fix by building the clip solid from a rectangle on the slope
plane that extends slightly past the face edge (1 project
unit margin) rather than the exact face footprint. Adjacent
slope solids now volumetrically overlap at the ridge instead
of sharing a boundary face, which OCCT handles correctly.

Generated with the assistance of an AI coding tool.
2026-06-01 07:44:49 -05:00
Gorgious56 de29d12b00 Fix fillet partner missing from wall unjoin gizmo
GizmoWallUnjoinSingle.poll accepts fillet-corner walls via the looser
tool.Parametric.is_path_connectable_wall predicate (fillet corners
have no LAYER2 usage by IFC spec, but they still participate in
IfcRelConnectsPathElements). The partner filter inside
_iter_path_connections used the stricter tool.Blender.Modifier.is_wall
(LAYER2-only), so adjacent LAYER2 walls silently dropped their
fillet-corner partners from the connection list — the unjoin icon
appeared when the fillet wall itself was selected but not on either
of its LAYER2 neighbours.

Switch the partner filter to is_path_connectable_wall so host and
partner predicates match. Add a regression test for the fillet case
and an AST forward-compat guard pinning the predicate symbol so a
future "tidy the imports" can't silently re-introduce the asymmetry.

Generated with the assistance of an AI coding tool.
2026-06-01 14:42:57 +02:00
Gorgious56 9440bafc32 Add array parametric edit lifecycle + GizmoArrayEdition / Child
Ports the array parametric-edit lifecycle, gizmo group, child guard,
per-layer ARRAY entry icons, and the array bbox decorators
(preview + selection highlight + layer-children) from gizmos-8088.
Restores the array_gizmo icon's positioning + visibility in the
framework's parametric edit row.

Registry (tool/parametric.py):
* EDIT_TYPES adds ParametricObject("array", supports_build_edit_lifecycle=True).
  _ArrayEditMixin in array.py feeds build_edit_lifecycle which auto-
  generates EnableEditingArray / FinishEditingArray / CancelEditingArray
  with the conventional bl_idnames the gizmo references.

tool/blender.py:
* Adds is_array predicate wrapper around tool.Parametric.is_array.
  The registry contract test test_every_entry_has_modifier_predicate
  enforces every EDIT_TYPES entry has a matching is_<name> wrapper on
  tool.Blender.Modifier.

array.py (+1130 LOC port from gizmos-8088):
* _ArrayEditMixin(ParametricEditMixinBase) drives the auto-generated
  enable / finish / cancel lifecycle.
* GizmoArrayEdition: validate + cancel + count display + +/- adjusters
  + method toggle + delete button + per-layer ARRAY entry icons
  (preallocated pool of MAX_LAYER_GIZMOS=8).
* GizmoArrayChild: child-array gizmo for the array-replica case.
* EditArrayFromChild: resolves the spawning layer via
  tool.Array.get_child_layer_index so clicking a child's array gizmo
  opens the layer that produced that child rather than always layer 0
  (the gizmos-8088 source itself hardcoded item=0; HEAD has the helper
  to do it right).
* New operators: EnableEditingArrayItem, ArrayParentGizmoClick,
  ArrayGizmoClick, ToggleArrayMethod, RemoveArrayLayerFromEdit,
  InputArrayCount, AdjustArrayCount.

prop.py: BIMArrayProperties gets per_child_opening BoolProperty
(when the array parent fills a host, give each child its own
opening + filling pair).

Bug fix: guard update_relating_array_from_object against the
cleanup-time None set. _finish_one writes relating_array_object = None
to clear the source-array reference; that fired the update callback,
which dispatched bpy.ops.bim.enable_editing_array(item=self.is_editing).
With is_editing just flipped to False, the bool coerced to 0 and
re-opened layer-0 edit immediately after every validate. The guard
short-circuits on None; item is also fixed to 0 (the bool-as-layer-
index was always meaningless for the legitimate user-pick path).

decorator.py (+312 LOC, all ports from gizmos-8088):
* bbox_world_edges / draw_polyline_segments / _BBOX_EDGES - shared
  geometry helpers usable across array decorators.
* draw_array_layer_children_bbox - green wireframe bbox per child of
  one array layer, drawn inline from a gizmo's draw() so the highlight
  tracks the hover cursor without POST_VIEW lag.
* ArrayPreviewDecorator - faint cyan ghost bboxes at each future
  array instance during the edit lifecycle (offset math mirrors
  Model.regenerate_array, gated on props.is_editing).
* ArraySelectionHighlightDecorator - bounding-box overlay surfacing
  the array family of the selected object. Child selected -> parent
  in special color + siblings in unselected color; parent selected
  (idle) -> all children in unselected color. TokenCache-backed.

handler.py: imports + uninstall/install the 2 always-on decorators in
_install_viewport_overlays. Both self-poll, so installation has no
cost when no array is selected / in edit mode.

Registration (bim/module/model/__init__.py):
* Adds the 3 lifecycle classes generated by build_edit_lifecycle
  (CancelEditingArray, EnableEditingArray, FinishEditingArray) -
  they exist as module-level names but are only visible to Blender's
  operator registry when included in the classes tuple.
* Adds the 8 new operators + 2 new gizmo groups in alphabetical order.

gizmos.py: restores the array_gizmo icon position + visibility block
in BaseParametricGizmoGroup.update_editing_gizmos. Was force-hidden
in c250b2c1a because no array gizmo existed; the icon's plumbing
comes back online now that GizmoArrayEdition is registered.

Verified by test/bim/test_parametric_registry.py: all 8 tests pass -
enable/finish/cancel ops resolve, PropertyGroup attached, is_array
predicate present, predicate is total on non-matching elements.

Generated with the assistance of an AI coding tool.
2026-06-01 14:39:58 +02:00
falken10vdl 431cf435ef Fix assign_container in spatial.py (#8079)
ifc.get_object(element) can return None for IFC elements that aren't loaded as Blender objects (e.g., decomposed sub-elements). 
The loop now skips those instead of passing None into collector.assign().

Cheers!
2026-06-01 07:02:28 -05:00
Tiago Azevedo a433f56337 Fix sign of temporary offset restore in sweep_along_curve
The temporary-offset workaround (#7408, commit bd57cc8735) subtracts the
directrix centroid (`mean`) from the curve points before building the
sweep near the origin, then must add it back to restore the original
location. The restore negated the sign — `Move(-mean)` instead of
`Move(+mean)` — placing the swept solid at -mean (mirrored through the
origin) rather than its true position.

Only triggers for polyline directrixes (`is_polyhedron()`) whose centroid
is more than 100 m from the origin (`mean.norm() > 1e2`), so models
centered near the origin are unaffected. Models that keep absolute site
coordinates (e.g. many Revit/ODA IFC exports) render affected swept
solids — reinforcing bars, pipes — at a mirrored phantom location far
from the rest of the model.
2026-06-01 11:42:39 +02:00
Gorgious56 0d3543fa31 Drop duplicate _path_connection_location_world in wall.py
PR3 shipped tool.Wall.path_connection_location_world; the local
_path_connection_location_world added in PR4 commit 70845e4dd
duplicated the same logic. The only caller in wall.py already uses
the tool method (line 3687 area), so the local helper has been
dead code since the migration in 7e5e7b8d6 routed _get_wall_geom_cached
to tool.Wall.read_geometry. Drop it.

Generated with the assistance of an AI coding tool.
2026-06-01 10:48:09 +02:00
Gorgious56 e764559133 Route _has_material_styles through tool.Root.has_material_styles
Pre-existing architectural smell on v0.8.0: core/root.py.copy_class
called a module-level _has_material_styles helper that did
ifcopenshell.util.element.get_materials() directly, bypassing the
Prophecy mock seam that every other branch in copy_class flowed
through. Symptom: test/core/test_root.py::TestCopyClass::
test_AAAAAAAAAAAA passed mock strings into copy_class, the helper
called .is_a() on the string, AttributeError.

Move the check to tool.Root.has_material_styles (paired with
assign_body_styles — they're called in sequence as "is there a
material style? if not, assign body style"). core/root.py now
calls root.has_material_styles(new) like every other dependency,
fixing the test failure and dropping the ifcopenshell.util.element
import that was the only consumer of the ifcopenshell import at
module load in core/root.py.

* core/tool.py: add abstract has_material_styles to Root interface.
* tool/root.py: add concrete classmethod near assign_body_styles.
* core/root.py: replace _has_material_styles helper call site with
  root.has_material_styles; drop the local helper and its import.
* test/core/test_root.py: add the new mock expectation
  root.has_material_styles("element").will_return(False) before the
  existing assign_body_styles expectation.

Generated with the assistance of an AI coding tool.
2026-06-01 10:47:57 +02:00
Dion Moult d390911d75 build_osx: build BonsaiViewer on macOS via build-all.py
Linux (build_rocky.yml) and Windows (win/build-all-win.py) already pass
BUILD_BONSAIVIEWER=ON when building from source; macOS was the odd one
out. Three small changes to bring it up to parity:

1. .github/workflows/build_osx.yml — \`brew install qt\` (Qt6 with Svg)
   in the Install Dependencies step, then set QT_DIR=\$(brew --prefix
   qt) and BUILD_BONSAIVIEWER=ON in the Run Build Script env.

2. nix/build-all.py — install_qt6() now honours a pre-set QT_DIR.
   Before this change get_qt6_aqt_config() raised on non-Linux,
   blocking bonsai builds on macOS/Windows from ever using a
   system-provided Qt6. We now validate that QT_DIR points at a real
   Qt6 install (probes lib/cmake/Qt6/Qt6Config.cmake) and skip the aqt
   download path if so. Linux flow is unchanged: when QT_DIR is unset
   the function falls through to the existing aqtinstall path.

build_osx.yml stays workflow_dispatch-only — slow run (~1h with
ccache, longer cold), so manual fire when wanted. Cherry-pick this
file + nix/build-all.py to v0.8.0 to make the workflow dispatchable
against the ifcviewer-wgpu branch before the squash lands.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 18:41:39 +10:00
Dion Moult 57a94095e8 ci: wgpu mac — drop the bare \-j\ flag
Ninja rejects \`-j\` without a numeric argument (unlike make, which
treats bare \`-j\` as unlimited parallelism). Build step exited with
\`ninja: fatal: invalid -j parameter\`. Drop the \`-- -j\` tail
entirely; Ninja already parallelises across available cores by
default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 18:26:29 +10:00
Dion Moult 025e60e635 ci: wgpu mac — cover ifcviewer-wgpu-minimal + wgpu-mem-probe in paths filter
The first run after BUNDLE DESTINATION fix didn't trigger because that
commit only touched src/ifcviewer-wgpu-minimal/CMakeLists.txt, which
the workflow's paths: list didn't cover. Add the two sibling
subprojects (-minimal and wgpu-mem-probe) since they participate in
the same configure pass.

(Manual workflow_dispatch is still unavailable until this workflow
lands on the default branch — GitHub gates the "Run workflow" button
on the default branch's copy of the file.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 18:20:30 +10:00
Dion Moult 07825b7442 ifcviewer-wgpu-minimal: add BUNDLE DESTINATION for macOS install rule
The target has MACOSX_BUNDLE ON, which on Darwin requires
install(TARGETS) to specify a BUNDLE DESTINATION — CMake validates
that at configure time, even when nobody runs `make install`. The
macOS CI configure step failed with:

    install TARGETS given no BUNDLE DESTINATION for MACOSX_BUNDLE
    executable target "IfcViewerWgpuMinimal".

Set BUNDLE DESTINATION bin alongside RUNTIME DESTINATION bin so both
platforms install into the same spot. No behavioural change on Linux
(no MACOSX_BUNDLE) — verified locally.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 18:16:19 +10:00
Dion Moult 317dbaa440 ci: wgpu mac — install Boost on the runner
Top-level cmake/CMakeLists.txt:328 does an unconditional
find_package(Boost REQUIRED COMPONENTS program_options regex thread
date_time iostreams) before the BUILD_BONSAIVIEWER gate, so we have
to install it on the runner even though IfcViewerWgpu itself doesn't
touch Boost. Removed via task #12 (extract ifcviewer-core) later.

Other unconditional finds in the top-level CMake (manifold,
nlohmann_json, USD, RocksDB, zstd) are already gated on flags that
default to OFF — no action needed for those.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 18:10:24 +10:00
Dion Moult b103563c8d ci: wgpu mac — disable IfcGeom/CGAL/OCCT; drop ctest -R filter
Two iterations on the first CI run:

1. The top-level cmake/CMakeLists.txt unconditionally find_package's
   CGAL (line 217) and OpenCASCADE (222) before our BUILD_BONSAIVIEWER
   gate kicks in, so configure failed with "Could NOT find CGAL". Pass
   BUILD_IFCGEOM=OFF + BUILD_IFCPYTHON=OFF + BUILD_CONVERT=OFF +
   BUILD_EXAMPLES=OFF + BUILD_GEOMSERVER=OFF + WITH_OPENCASCADE=OFF +
   WITH_CGAL=OFF + COLLADA_SUPPORT=OFF so all the heavy deps stay out
   of the configure step. Verified locally on Linux.

2. The ctest -R "wgpu" filter was case-sensitive and the Catch2 test
   names begin with capital "Wgpu" (e.g. "WgpuSelectionState starts
   empty..."), so it matched zero tests and ctest exited with "No
   tests were found". The standalone wgpu config only builds the
   wgpu tests anyway, so the filter is unnecessary — drop it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 18:03:40 +10:00
Dion Moult 53e9240702 ci: wgpu sanity check on macOS
Lightweight workflow that compiles IfcViewerWgpu (the static lib) on
macOS arm64 + runs the wgpu state tests. Skips IfcViewerWgpuMinimal
because createSurface has no Metal path yet (task #32); the platform
surface blocks in WgpuViewportWindow.cpp are wrapped in
#if defined(Q_OS_LINUX) so the lib itself compiles cleanly on macOS.

Goal: catch portability regressions in the wgpu source on Apple
Silicon without paying for build_osx.yml's full IfcGeom + OCCT +
Python wheel pipeline. ~5 min vs hours.

Triggers on push/PR that touches src/ifcviewer-wgpu, the shared
headers it depends on, the top-level CMake, or this workflow itself.
Also workflow_dispatch for manual runs.

Hoists the Catch2 fetch in cmake/CMakeLists.txt out of the
BUILD_BONSAIVIEWER gate so the standalone wgpu config
(BUILD_BONSAIVIEWER=OFF + BUILD_BONSAIVIEWER_WGPU=ON +
BUILD_BONSAIVIEWER_TESTS=ON) can build tests without dragging the
whole bonsai/IfcGeom tree in. Default remains OFF, so default builds
stay offline-capable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 17:53:41 +10:00
Dion Moult 42ab97b134 tests: pass WITH_MESH_OPTIMIZER into test_lod_builder
LodBuilder.cpp guards its real body behind #ifdef WITH_MESH_OPTIMIZER
(the stub is `return;`). The IfcViewer static lib propagates the
define via target_compile_definitions, but test_lod_builder compiles
LodBuilder.cpp standalone (it doesn't link IfcViewer), so the test
silently exercised the no-op path. summariseLods and buildLods cases
asserted on the post-build state and saw zero LOD1 output.

Pre-existing regression since 884e7ba32 ("Make meshoptim optional");
adds the define to the test target directly so the real build path
runs. 5/5 LOD cases pass after.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 17:40:55 +10:00
Dion Moult 379f913f65 wgpu tests: port selection + visibility state coverage
Two Tier-1 unit binaries under src/ifcviewer-wgpu/tests/ — same Catch2
+ CTest harness as the surviving GL-side tests, gated by
BUILD_BONSAIVIEWER_TESTS.

- test_wgpu_selection: 17 cases / 71 assertions covering replace, add,
  remove, toggle, clear, contains, count, selectionIds, fillFlagsArray,
  active-id semantics, dirty-bit, and id == 0 sentinel handling.
- test_wgpu_visibility: 8 cases / 27 assertions covering hide, show,
  clear, isHidden, hiddenIds, idempotence, and the 0 sentinel.

The wgpu state classes have a deliberately simpler shape than the GL
ones (no Q_OBJECT, no signals — replaced by a dirty bit; no bulk
set/add/remove methods — bulk behaviour lives in the viewport verbs).
One *intentional* behavioural difference is documented in the test:
add(id) steals active in the wgpu API, where GL's addToSelection kept
the prior active. Each pick should drive the properties panel to the
most recently touched object.

Bulk hide/isolate/show-all semantics live in WgpuViewportWindow, which
composes WgpuVisibilityState + the model instance lists; those are
integration-level, not Tier-1, so they're not covered here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 17:39:34 +10:00
Dion Moult 2981500b3b Route bonsai through wgpu; delete the GL backend
Bonsai now drives the wgpu viewport for both sidecar and direct-IFC
loads. The GL viewer and its supporting state classes are gone.

SceneLoader rewire:
- Takes WgpuViewportWindow* instead of ViewportWindow*.
- Sidecar path reads metadata only (readSidecarMetadataOnly) and hands
  the StreamingSidecar off to the new applyCachedModel. Field accesses
  inside applySidecarData go through .meta.
- Direct-IFC path uses the wgpu A-path (upload{Mesh,Instance}Chunk +
  finalizeModel). The applyLodExtension call is dropped — wgpu has no
  live LOD1 splice; LOD1 still lands in the on-disk sidecar for the
  next open.

Bonsai migration:
- ViewportWindow → WgpuViewportWindow across MainWindow, Measurement,
  SessionState, and every modules/*/{Commands,Panel,View}.{h,cpp} —
  116 sites total. Same s/OverlayRenderer::/WgpuOverlayRenderer::/
  rename, 12 sites.
- Includes flipped from ../ifcviewer/ViewportWindow.h to
  ../ifcviewer-wgpu/WgpuViewportWindow.h. OverlayRenderer.h include
  dropped (transitively reached via the viewport header).
- BonsaiViewer links IfcViewerWgpu in addition to IfcViewer for the
  duration of the migration; the GL-side IfcViewer also publicly links
  IfcViewerWgpu so SceneLoader can resolve WgpuViewportWindow.

GL backend deletion:
- src/ifcviewer/ViewportWindow.{cpp,h}, BvhAccel.*, OverlayRenderer.*,
  Selection.*, Visibility.* all gone.
- src/ifcviewer-minimal/ removed entirely (MinimalWindow drove the GL
  viewport).
- src/ifcviewer/tests: test_bvh_accel, test_selection, test_visibility
  removed. The first has no replacement (wgpu doesn't use a per-instance
  BVH); the latter two are ported separately. test_lod_builder,
  test_sidecar_cache, test_instanced_geometry, test_federation remain
  (backend-agnostic).
- IfcViewer's CMakeLists drops OpenGL, Qt::OpenGL, Qt::Widgets — none
  of the surviving translation units reach for them.

Build flag plumbing:
- BUILD_BONSAIVIEWER now auto-enables BUILD_BONSAIVIEWER_WGPU since
  SceneLoader requires the wgpu lib for its WgpuViewportWindow* arg.
- The wgpu subprojects add_subdirectory ahead of the GL one so
  IfcViewerWgpu exists when IfcViewer's link evaluates.
- src/ifcviewer-minimal subdir reference removed from cmake/CMakeLists.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 17:38:33 +10:00
Dion Moult 9c067d1d0e wgpu: bonsai-ready API surface + direct-IFC ingestion + streaming-always
Make the wgpu viewport ready for bonsai's verb actions, federation
refresh, and tool routing — i.e. callable from an outside host, not
just from the minimal viewer's own hotkeys.

Surface additions on WgpuViewportWindow:
- Qt signals: objectPicked, frameStatsUpdated, surfacePickedInTool,
  toolModeChanged, toolBackspacePressed.
- FrameStats struct + rolling 60-sample frame-time window for the
  fps field; emit at end of render() so external listeners see fresh
  numbers in the same tick.
- InstanceLookup struct + findInstance(object_id, ...) const for the
  measurement tools' O(1) object → (model, mesh, placement) resolve.
- Federation hooks (setFederatedFalseOrigin / setModelCoordinateOperation
  / setModelTransformation) + per-model coordinate_operation_meters /
  model_transformation_meters fields on WgpuModelGpuData. Implement
  composeInstanceFromPlacement + recomposeAndUploadModel so each setter
  actually applies — model recompose runs in double, casts to float for
  the GPU upload, and refreshes per-chunk world AABBs. meshLocalToGlobal
  now composes coordinate_operation · placement properly.
- showModel / hideModel for per-model visibility, plus element-level
  verbs (hideSelectedElements / isolateSelectedElements / showAllElements
  / invertElementVisibility) and setSelectedObjectId / cameraState() /
  projectionOrtho() / toggle{Area,Length,Volume}Tool wrappers.
- Section-cutting methods (toggleSectionTool / clearSectionPlanes /
  sectionToolActive) moved to public so bonsai's Commands.cpp can call.
- QVector3D overload of computeObjectAabb to match the GL signature.
- ToolMode::None → ToolMode::NoTool (X11 macro collision avoidance).

Direct-IFC ingestion (A-path), mirrors the GL streaming push API:
- uploadMeshChunk / uploadInstanceChunk stage into pending_direct_loads_
  using the same vertex quantisation as SidecarBuilder so direct-load
  and sidecar-load produce byte-identical buffers.
- finalizeModel wraps the staged data in a file-less StreamingSidecar,
  routes through the existing applyCachedModel chunk planner, then
  gathers per-chunk vertex+index bytes from memory and feeds
  applyStreamedChunk synchronously. Every chunk lands is_resident=true
  immediately (no disk I/O to defer).

Streaming collapse:
- Delete the applyCachedModel(SidecarData) full-load path entirely.
- Rename applyCachedModelStreaming → applyCachedModel; loadSidecar
  always uses the metadata-only reader. Drop the --streaming CLI flag
  from IfcViewerWgpuMinimal and the streaming_enabled_ field.

WgpuSelectionState::ids() → selectionIds() so bonsai's
`viewport_->selection().selectionIds()` compiles unchanged.

Eigen3 added as a public dep of IfcViewerWgpu for the federation
matrices.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 17:36:02 +10:00
Gorgious56 a3f92eb427 Merge pull request #8133 from Gorgious56/bonsai/parametric-framework-features
Bonsai/parametric framework features
2026-06-01 09:20:51 +02:00
Gorgious56 453e6dc1cc Add behaviour-contract tests for PR4 surfaces
Three test files covering PR4's new surfaces — preview registry,
wall-gizmo poll behaviour, fillet operator registration. Every test
walks the live registry or class hierarchy instead of hard-coding
preview keys, operator names, or helper function names, so adding a
new preview / wall gizmo group / fillet operator exercises the same
invariants without test edits.

test_preview_base.py (6 tests):
* RegistryContract: every PREVIEW_CANCEL_OPS entry resolves to a
  callable cancel operator on bpy.ops.bim.
* GetPreviewPropsTolerance: get_preview_props returns None for
  contexts without a scene (regression guard for the SimpleNamespace
  bug fixed in commit ee63137c6).
* ActivationCycle (registry-driven loop): any_preview_active toggles
  with each registered preview's is_active flag;
  discard_pending_previews clears every active flag across every
  registered preview.
* SaveOnDiscardWired: locates the bim.save_project operator
  dynamically and verifies its execute path references the discard
  helper by its actual __name__.

test_wall_gizmo_poll_gate.py (4 tests):
* WallGizmoGroupsHideDuringPreview: walks the wall module for
  bpy.types.GizmoGroup subclasses (skips preview-owner exceptions
  whose bl_idname contains 'preview'), mocks any_preview_active to
  True, and asserts every discovered gizmo's poll returns False.
* BaseParametricGizmoPollHidesDuringPreview: mirrors the test for
  the cross-feature parametric framework base class.

test_fillet_operators.py (3 tests):
* FilletOperatorsRegistered: at-least-four-ops + every-discovered-op-
  is-callable. Catches accidental deregistration.
* EnableRejectsIneligibleSelection: poll returns False without a
  selection so the operator is greyed-out in menus.

State-clearing tests via bpy.ops.bim.cancel_wall_fillet_preview() are
deliberately omitted — the operator early-returns when context.screen
is unattached and prior tests in the model lane can leave the screen
in that state, making the dispatch path inherently flaky. Live testing
covers the behaviour.

Net: 13 tests pass cleanly in both single-file and full model lane.

Generated with the assistance of an AI coding tool.
2026-06-01 08:35:26 +02:00
Bruno Perdigão 728026d3f0 Remove debug print 2026-05-31 22:30:56 -03:00
Bruno Perdigão 3f270df11e Add more no headless test for snap 2026-05-31 22:26:16 -03:00
Bruno Perdigão 792a0c7da1 Merge tests into a single file 2026-05-31 22:26:16 -03:00
Bruno Perdigão 5856d29fbe Add test files and scripts 2026-05-31 22:26:16 -03:00
Bruno Perdigão cd482a7874 Initial implementation of tests for modal operators 2026-05-31 22:26:16 -03:00
Dion Moult e80897886d wgpu: per-chunk OOM cooldown + continue past blocked candidates
Fixes two coupled streaming pathologies on working-set > pool scenes:

1) The candidate loop used `break` when a candidate couldn't fit even
after eviction. Comment justified it with "sorted by priority, lower
candidates can't beat it either" — true for *priority* eviction, but
the failure is *size-based fitting*. A 31 MB candidate that doesn't
fit in 24 MB largest-free was starving the entire per-frame budget,
including smaller candidates that would have fit happily. Replaced
with `continue`.

2) Same blocked candidate re-entered the candidate list every frame
forever, spamming `[blocked]` and (worse, on web) paying for the same
byte-range fetch over and over when apply-time OOM happened. Added
`blocked_cooldown_until_frame_idx` on the chunk: when OOM strikes at
enqueue *or* apply, the chunk is skipped from candidate gathering for
~3s. Web-friendly cap of one wasted fetch per 3s per chronic chunk
instead of per-frame. Cooldown expires naturally; if the pool layout
changes within the window (other chunks evicted, fragmentation
coalesces) the chunk re-enters automatically.

Also added eviction-attribution + chunk thrash detection (gated behind
WGPU_STREAM_EVICT_LOG=1) to confirm A→B→A 2-cycles vs simple
sacrificial-victim cycles. Quietened the steady-state stream debug
dump — moved the verbose multi-line "missing/resident/bottom" snapshot
behind WGPU_STREAM_DEEP_DEBUG=1 with a wider 300-frame interval, and
added a single-line `[stream]` health summary every ~5s in interactive
mode. Removed the hardcoded one-off "brace.ifc bracing all
chunks" dump that was investigation scaffolding for a now-closed bug.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 10:01:10 +10:00
Dion Moult 699f22b502 wgpu: length measurement tool (L hotkey, adaptive 1/2/3/4+ point readout)
Ports Bonsai's LengthMeasurement onto WgpuViewportWindow as a new
WgpuLengthMeasurement class. Each LMB appends a world-space pick point
and the readout adapts to the running count:

  1 pt   → laser-measure: coplanar-patch BFS on the click's surface
           projects every patch vertex into the surface's own tangent
           basis to get face extents (X/Y/Z bars dashed in world space),
           plus ENH coords for the picked point, plus a vertical
           raycast for floor/ceiling distance on horizontal surfaces.
  2 pts  → distance A→B + axis-coloured ΔX/ΔY/ΔZ stair-step + dashed
           perpendicular projection when both picks landed on
           near-parallel surfaces.
  3 pts  → angle at middle vertex + triangle area + perimeter.
  4+ pts → polygon area via best-fit-plane shoelace (Jacobi-3x3
           eigendecomp inline; no Eigen dep) or fan-triangulated
           fallback for non-planar loops, plus closed-loop perimeter.

Backspace / Del removes the last point; Esc / L again exits.

Dependencies layered in:

- pickMeshLocalAt now refines the AABB-coarse pickSurfaceAt hit into a
  real triangle hit via Möller-Trumbore against the picked instance's
  CPU mesh shadow. Without this the BFS seeds with whatever triangle
  is closest to the bounding-box corner — producing patches and
  extents shaped like the AABB instead of the surface.
- meshLocalToGlobal: applies the instance's placement_transformation
  only (no per-model CoordinateOperation in wgpu yet). ENH equals
  IFC-world for non-federated loads, which is what the minimal viewer
  handles.
- raycast: brute-force world-AABB cull + Möller-Trumbore over the CPU
  mesh shadow. Used by the laser-measure ceiling/floor distance.
- ToolMode gains Length; click handler routes plain/Alt LMB through
  onLengthPick, Backspace through onLengthBackspace. Marquee-arm is
  gated off in Length mode.

Volume HUD now shows "Volume: 0.0000 m³  (0 objects)" the moment V is
pressed, matching how A primes "Area: 0.0000 m²" — gives the user a
visible cue the tool is active before any selection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 08:44:12 +10:00
Gorgious56 f6e95c8e8e Bonsai Makefile - pin deepdiff<9.1
deepdiff 9.1.0 added cachebox<6,>=5.2 as a direct runtime dep.
cachebox 5.2.3 only publishes macOS x86_64 wheels for macosx_10_12+,
incompatible with the macos py311 build's --platform macosx_10_10_x86_64.
The daily build's linux-wheel safeguard fires when the resulting
cachebox-*-manylinux_*.whl leaks into the macOS / windows wheels folder
(builds run on ubuntu-latest and cross-build via pip download --platform).

Pin deepdiff to <9.1 (resolves to 9.0.0, no cachebox transitive dep) as
the minimal hotfix. Long-term cleanup: bump the macos py311 platform tag
from 10_10 to 10_13 (matching py312/py313) and re-flag this line with the
standard \$(PYPI_PLATFORM) --only-binary=:all: pattern used by brickschema
and python-socketio.

Partly generated with the assistance of an AI coding tool.
2026-05-31 21:33:28 +02:00
Gorgious56 ee63137c6c Discard previews on IFC save + harden preview-active gate
Save-path:
* SaveProject._execute (project/operator.py) now calls
  preview_base.discard_pending_previews(context.scene) right after
  tool.Parametric.commit_pending_edits(). Previews are session-
  transient — discard rather than commit. Sibling gizmo polls gate
  on each preview's is_active flag; a stuck flag persisted through
  the save would silently hide them on reload. Mirrors the pattern
  already in gizmos-8088.

Preview-active gate hardening:
* preview_base.get_preview_props tolerates contexts without a
  ``scene`` attribute. Pre-existing tests use SimpleNamespace mocks
  for the context; the previous getattr(context.scene, ...) raised
  AttributeError before the inner default kicked in.

Test update:
* test_wall_header_refresh.test_geom_generation_invalidates_wall_geom_cache
  patches tool.Wall.read_geometry instead of the now-deleted local
  wall._read_wall_geometry (commit 7e5e7b8d6 migrated the call site).

Generated with the assistance of an AI coding tool.
2026-05-31 19:10:48 +02:00
Ryan Schultz c83b4eb69f Restore pre-aggregate selection on exit; deselect on unsupported profile
When override_mode_set_edit encounters an unsupported profile (Couldn't
import profile), deselect the object so Tab continues to cycle cleanly.

Also restores the selection that existed before entering aggregate mode
when finally tabbing out, via save/restore_previous_selection().
2026-05-31 07:41:03 -05:00
Ryan Schultz 5eef433abf Deselect geometry after exiting item mode in aggregate context
Following the pattern from 586f9be077, deselect the active object after
exiting item mode so Tab continues to cycle cleanly. Also deselects
parametric LAYER1/LAYER2 items that cannot be edited directly, avoiding
the need to manually deselect before Tab-cycling out of aggregate mode.
2026-05-31 07:25:58 -05:00
Dion Moult 8761f9ac46 wgpu: area measurement tool (A hotkey, BFS coplanar patch + cyan highlight)
Ports Bonsai's AreaMeasurement onto WgpuViewportWindow as a new
WgpuAreaMeasurement class. Each LMB pick resolves to (instance,
triangle), BFS-expands the coplanar patch (dot(normal, seed_normal)
> 0.9999, ~0.81° tolerance), and toggles it in/out of the running set.
Alt+LMB skips BFS for single-triangle accumulate. Connected-components
sweep over the selected set produces one "X.XXXX m²" label per patch
at its area-weighted centroid in world space; HUD shows the running
total + triangle count.

Dependencies layered in:

- WgpuOverlayRenderer.setHighlightTriangles / encodeHighlightTriangles:
  translucent world-space triangle list (cyan @ 0.45 alpha), depth-
  tested but depth-write off so the corner gizmo + labels still sit
  on top.
- WgpuViewportWindow.pickMeshLocalAt: reuses pickSurfaceAt for the
  world hit, then inverts the instance's composed transform to express
  it in mesh-local space — what the BFS needs. Uses the live map key
  (`mid`) rather than InstanceCpu.model_id, which is whatever the GL
  streamer wrote at sidecar-write time and goes stale across sessions.
- WgpuViewportWindow.readbackMeshTriangles: CPU mesh shadow lookup.
  The shadow itself is populated during the same dequant pass that
  computes mesh-local volume — applyCachedModel for full loads and
  applyStreamedChunk for streaming, so the BFS has data the moment
  the user can pick it.

WgpuModelGpuData gains a MeshTriangles vector indexed by mesh_id;
doubles per-vertex CPU memory (12 B/vert) but skips wgpu mapAsync
plumbing for now. Bounds-check at pick time gracefully no-ops when a
stale sidecar field is out of range.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 22:12:26 +10:00
Dion Moult 0774398d4e wgpu: volume measurement tool (V hotkey, selection-driven HUD + per-object labels)
Ports Bonsai's volumeOfObjects + volumesPerObject onto WgpuViewportWindow.
Mesh-local volumes are precomputed at applyCachedModel via signed-
tetrahedra-from-origin (dequantising positions from the 12 B/vertex GPU
layout); per-instance volume is just the cached local × |det(placement)|.
No GPU readback — measurement is O(K) in the selection size.

Streaming path computes volumes per-chunk as they arrive — fills any
mesh whose chunk just delivered, then re-runs updateVolumeReadout if
the user is staring at a Volume readout while the geometry pages in.

UX matches GL: V toggles, Esc exits, selection-driven (LMB pick / marquee
/ Shift/Ctrl set ops all funnel into updateVolumeReadout). HUD shows
total + count; one overlay label per object at its AABB centre, capped
at 200 to keep the label-texture cache bounded on large marquees.

Side fixes layered on the label overlay:
- O(1) AABB lookup via object_id_to_instance instead of linear-scanning
  every model's instance list per selected object.
- Label texture cache evicts entries not touched this frame, so churning
  through "X.XXXX m³" strings doesn't pin GPU memory.
- DrawRec stores the WGPUBindGroup handle by value rather than a
  LabelTexture* pointer into the QHash — getOrCreateLabelTexture can
  rehash the table and invalidate every captured pointer, which crashed
  large marquee selections with BindGroup-no-longer-alive.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 20:57:44 +10:00
Gorgious56 7e5e7b8d6a Drop wall.py local read_geometry + validate dupes + relax gates
Two cohesive cleanups in one commit.

A. Migrate wall.py to PR3-absorbed tool methods (fixes bug 4: pen icon
missing on fillet corner walls):

PR3 shipped tool.Wall.read_geometry + tool.Wall.validate_for_parametric_edit
but wall.py kept local duplicates predating that work. The local
_read_wall_geometry guards on tool.Blender.Modifier.is_wall (LAYER2-only)
while the tool method guards on tool.Parametric.is_path_connectable_wall
(LAYER2 OR fillet corner). Consequence: _get_wall_geom_cached → local
_read_wall_geometry returned None for every fillet corner →
GizmoWallFilletReedit.position_gizmos hit `if geom is None: hide` →
pen icon was unreachable for every fillet corner the user created.

Three _read_wall_geometry callers migrated to tool.Wall.read_geometry
(_read_wall_state_into_props, _get_wall_geom_cached,
GizmoWallJoinIntersection.position_gizmos). Two
_validate_wall_for_parametric_edit callers migrated to
tool.Wall.validate_for_parametric_edit (_maybe_resync_wall_props_from_ifc,
EnableEditingWall._execute). Local helpers deleted; docstring references
updated.

B. Drop over-restrictive gizmo gates (fixes bug 1: join icons missing
when walls intersect away from endpoints):

GizmoWallJoinIntersection.position_gizmos no longer hides itself when
the projected intersection lands further than MAX_DISTANCE_TO_ENDPOINT_
FACTOR (0.75 wall lengths) from any endpoint. The remaining
PARALLEL_DOT_THRESHOLD (cos 2°) gate via project_axis_intersection
returns None for near-parallel walls and is the only correctness bound;
distance from endpoints is a UI concern, not a geometric one.

GizmoWallFilletReedit.poll drops the has_a / has_b ConnectedFrom +
ConnectedTo guard — the IsFilletCorner pset is the authoritative signal.
EnableWallFilletPreviewFromCorner.execute already separately validates
both neighbour connections and reports a user-facing error if either
side is disconnected.

Generated with the assistance of an AI coding tool.
2026-05-31 12:15:39 +02:00
Gorgious56 788d4fe8e8 Hide sister gizmos during preview + ESC cancels + DRY wall polls
Three live-session regressions surfaced after the fillet feature
landed.

Sister gizmos competed with the active preview:
* preview_base.any_preview_active(context): new helper iterates the
  PREVIEW_CANCEL_OPS registry and returns True if any preview is open.
  Future previews registered there automatically gate sister gizmos.
* BaseParametricGizmoGroup.poll (gizmos.py): short-circuits on
  any_preview_active so every parametric gizmo (door/window/stair/
  roof/railing/wall edition) hides during ANY preview.
* The 4 wall gizmo groups with explicit polls (GizmoWallAddOpening,
  GizmoWallExtendVertically, GizmoWallJoinIntersection,
  GizmoWallUnjoinSingle) + GizmoWallFilletReedit gain the same gate.

DRY: extract _wall_gizmo_poll_gate(context):
* 5 wall gizmo polls each duplicated the 2 pre-flight checks
  (viewport-gizmos enabled + no preview active). The helper centralises
  them — each poll becomes a single short-circuit line followed by its
  per-feature selection inspection.

ESC cancels the active preview:
* try_cancel_active_preview already existed in preview_base since PR3
  but had no caller. Hooked into OverrideEscape.execute (geometry/
  operator.py) as a new elif branch — same keymap that already cancels
  pen gizmo edit mode + item mode + edit mode + aggregate mode. Order
  in the branch chain matters: try preview cancel before falling back
  to try_canceling_editing_modifier_parameters_or_path so the in-
  flight preview wins over a stale modifier-edit cancel attempt.

Generated with the assistance of an AI coding tool.
2026-05-31 10:38:30 +02:00
Ryan Schultz a1c2aecf1b Add select_similar to type attribute panels
In BIM_PT_type_attributes and BIM_PT_object_attributes (when
the active object is a type), attribute value buttons now use
"type.<Attr>" as the selector key so the operator finds
matching occurrences via their relating type rather than the
occurrence's own (often unset) attributes.

Generated with the assistance of an AI coding tool.
2026-05-30 21:53:13 -05:00
Ryan Schultz d501970352 Add clipboard copy to SelectSimilarContainer operator
After selecting objects in the same container, copy a `location="Name"`
filter query to the clipboard and report it — consistent with the same
behaviour in SelectSimilarType, SelectSimilarAggregate, SelectIfcClass,
and SelectSimilarMaterial.

Generated with the assistance of an AI coding tool.
2026-05-30 17:41:50 -05:00
Dion Moult 3ac7a79b7a wgpu: overlay labels + HUD text (QPainter rasterise, content-cached)
Ports GL OverlayRenderer's setOverlayLabels + setHudText to wgpu.
Each unique string is rasterised via QPainter into a QImage (dark-grey
rounded background + white antialiased text) and uploaded as an RGBA8
texture; the cache is keyed by content + font size so identical
strings across frames are texture-free. Per-frame work is projection,
vertex assembly, and one draw per visible label.

Drawn last in the frame on the resolved surface so labels sit on top
of every other overlay (no depth-test). HUD uses pt 11 at top-left
matching GL; world-anchored labels use pt 9 centred at the projected
screen position.

WebGPU has no QOpenGLPaintDevice equivalent — the GL backend's two-
stage GL-rect + QPainter pass becomes one textured quad per item here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 08:40:54 +10:00
Dion Moult 716dba2244 wgpu: overlay points (sprite-style, quad-expanded with stroke halo)
Ports GL OverlayRenderer's setOverlayPoints API to WgpuOverlayRenderer.
Each point becomes a 6-vertex screen-space quad sized to inner_diameter
+ 2*stroke_extra; the fragment reads its per-vertex corner varying
instead of gl_PointCoord (WebGPU has no sized-point primitive). Sharp
inner/stroke transition + AA on the outer edge only, matching GL.

Single uniform slot per set — colors are global to the call, not per
point. Vertex buffer regrows 1.5× on demand so steady-state sets don't
re-allocate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 08:23:51 +10:00
Ryan Schultz fd96e6a4d2 Fix #8128: Fix filter_elements skipping groups after a zero-result facet_list
When a `+`-separated filter group returns no results, `FacetTransformer.facet_list`
was skipping the reset of `has_additive_facet_in_current_list` because the reset
was inside the `if self.elements:` guard. The stale flag caused the next group's
`add_default_elements()` to bail out early, leaving its element set empty and
silently dropping every subsequent group from the result.

Move the flag reset outside the guard so it always fires regardless of whether
the group produced any results.
2026-05-30 16:28:14 -05:00
Ryan Schultz 3dd3a0d70c Closes #8127: Add imperial location display to Placement panel
In the Placement panel, show Location and Rotation X/Y/Z
each on their own row beneath a header label. When the IFC
file uses imperial units, display a read-only feet-and-inches
label alongside each Location input field.

Generated with the assistance of an AI coding tool.
2026-05-30 14:08:53 -05:00
Ryan Schultz 2e5995176a Format stair lengths using IFC length unit
Display general and calculated stair parameters (Width,
Height, Tread Run, Tread Rise, Length, etc.) formatted
to the IFC file's configured length unit rather than
raw numeric values.

Generated with the assistance of an AI coding tool.
2026-05-30 12:03:50 -05:00
Gorgious56 2114c1d5d0 Add wall-fillet feature: operators, gizmos, decorator
End-to-end fillet flow on top of the helpers + recreate_wall hook
(landed in the previous commit). Users select two LAYER2 walls, click
the fillet entry icon, drag the live radius widget, and validate to
replace the corner with a curved LAYER2 corner wall (banana body).

Operators (5):
* EnableWallFilletPreview: 2-wall selection → validates LAYER2 +
  straight axis + zero-slope + intersect-or-joined state → seeds the
  preview props with a default radius computed from the shorter
  available leg.
* FinishWallFilletPreview: dispatches CreateWallFillet with the tuned
  radius; clears preview state on FINISHED, preserves it on failure so
  the user can re-tune without re-selecting.
* CancelWallFilletPreview: clears preview state, no IFC mutation.
* EnableWallFilletPreviewFromCorner: pen-icon re-edit on an existing
  fillet corner — pre-fills the preview from the corner's BBIM_Wall
  pset + walks the inverse graph to recover wall A and wall B.
* CreateWallFillet: deletes any prior corner + A↔B path connection,
  shortens A and B to the tangent points, instantiates a corner wall
  from A's type, unassigns the swept-layer material/type (the explicit
  banana body MUST own its geometry), assigns the dominant material,
  rebuilds the body, sets a straight 2-point chord axis, stores
  BBIM_Wall.IsFilletCorner+FilletRadius, reconnects A and B to the
  corner with NOTDEFINED on the corner's side.

Gizmo groups (2 new + entry icon on existing):
* GizmoWallFilletPreview: visible while a preview is active. Bundles
  a radius_dim widget at the arc apex, a trim_dim widget along wall A
  expressing the same DOF via the leg setback distance
  (trim = |radius| * tan(sweep/2)), and validate / cancel icons
  anchored above the apex in screen-up.
* GizmoWallFilletReedit: pen-icon entry on an existing fillet corner
  wall (single-selection, BBIM_Wall.IsFilletCorner set, both neighbour
  connections present). Mutually exclusive with an active preview.
* GizmoWallJoinIntersection now stacks a fillet entry icon
  (VIEW3D_GT_fillet → bim.enable_wall_fillet_preview) above the
  existing join/unjoin icon in the joined and intersect state branches.

Property + decorator infrastructure:
* prop.py: BIMWallFilletPreviewProperties (Scene-level draft) +
  BIMPreviewProperties umbrella with only the wall_fillet pointer.
  The umbrella is the seam preview_base.py (landed in PR3) already
  reads via getattr(scene, "BIMPreviewProperties", None).
* decorator.py: _stroke_lines_alpha helper + WallFilletPreviewDecorator.
  Polls is_active; renders leg projections + arc + arc-center
  construction lines from tool.Wall.compute_wall_fillet_geometry.
* __init__.py: registers operators + gizmo groups + property groups +
  wires Scene.BIMPreviewProperties.
* handler.py: WallFilletPreviewDecorator.install/uninstall in
  _install_decorators — always installed, self-polls on is_active.

Drive-by: extract gizmo.get_screen_up(billboard_rot) helper —
the local +Y of a billboard rotation is the camera's screen-up world
direction. Replaces 4 inline `billboard_rot @ Vector((0.0, 1.0, 0.0))`
sites added across the fillet feature's gizmo groups.

Generated with the assistance of an AI coding tool.
2026-05-30 13:04:49 +02:00
Dion Moult 1d26e5fb92 wgpu: overlay-line groups (stroke + dash, per-group dynamic uniform offset)
Ports GL OverlayRenderer's LineGroup API to WgpuOverlayRenderer. Each
group's segments are CPU-expanded into screen-space quads; the WGSL
fragment reproduces the GL pixel-distance stroke pick + arc-length
dash logic. One uniform slot per group, bound via dynamic offset so a
single bind-group services up to N groups.

No caller yet — sets up the API the wgpu measure tools (task #29) will
use. WgpuViewportWindow.setOverlayLines mirrors the GL viewport's
signature so the bonsai Measurement code can target either backend
through one interface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 20:38:59 +10:00
Dion Moult 814ae8304f wgpu: extract overlays (axis, pivot, section, marquee) into WgpuOverlayRenderer
WgpuViewportWindow.cpp had ~1100 lines of pipeline/shader/buffer plumbing
for the axis indicator, pivot gizmo, section visualizer, and marquee
drag rect. Mirroring the GL backend's split, that lives in its own class
now; the viewport keeps the camera/cull/draw loop and hands the renderer
a per-frame WgpuOverlayFrame snapshot for each encode call.

No behavioural change — pixel-identical screenshot on basic.ifcview.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 19:58:59 +10:00
Dion Moult 07913e2832 wgpu: marquee box-select (drag rect + Shift/Ctrl set ops)
Drag LMB in empty space to select every visible object whose pick-pixel
falls inside the rect. Mirrors GL ViewportWindow's marquee.

UI flow

  - LMB press in non-tool-consuming context arms the marquee. The
    cursor must move past kBoxSelectThresholdPx (5 logical) for it to
    become active — until then a release falls through to single-pick,
    so an unintentional micro-drag still picks under the cursor.
  - Press-time modifiers decide the set op so a mid-drag Shift release
    doesn't flip behaviour:
      plain  → selection.clear() then add every picked id
      Shift  → add to current selection
      Ctrl   → remove from current selection
  - Section tool intercepts plain LMB first (already wired); the
    marquee is mutually exclusive with it.

Rectangle pick

picksInRect(x, y, w, h):
  - Render the existing pick pass (R32UInt object_id + RGBA16F normal
    MRT) into the persistent pick attachments.
  - copyTextureToBuffer the rect region of pick_color_texture_ into
    box_pick_staging_buffer_ (regrown 2× on demand to fit the
    largest rect we've seen). R32UInt is a color format so partial
    sub-rect copies are allowed (unlike Depth32Float).
  - Iterate the mapped staging buffer, accumulate unique non-zero ids
    into an unordered_set, return.

Visual rect

A new marquee overlay pipeline draws the drag rect on the resolved
surface after the corner gizmo. Two passes per active frame share one
uniform buffer / bind group:

  fill    — 6-vert unit quad, vs_fill maps (0,1)² to NDC via
            rect_min/rect_max, fs_fill outputs color × fill_alpha
  outline — 24-vert thick-line quad (4 segs × 6 verts), uses the
            shared thick_line_clip + fs_main from THICK_LINE_HELPERS_WGSL
            so the rect outline has analytical AA without MSAA

Colour: Bonsai decorator_color_special (0.157, 0.565, 1.000) for the
axis-blue parity the user requested; outline alpha 0.95, fill alpha
0.20 of that so geometry behind the rect still reads.

Closes #41 and #61.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 16:46:30 +10:00
Dion Moult 0ed355154a wgpu: shared thick-line shader, Bonsai decorator palette, fatter section gizmo
Consolidation

THICK_LINE_HELPERS_WGSL — a macro that both AXIS_WGSL and SECTION_WGSL
prefix via adjacent string-literal pasting — holds:

  - VsOut: clip_pos + rgba colour + side_t for AA
  - thick_line_clip(p_start, p_end, t, side, viewport, line_width):
    the screen-space quad expansion with consistent perpendicular so
    the quad never collapses into a bowtie
  - fs_main: |side_t| + fwidth() coverage smoothstep — analytical
    1-pixel AA regardless of MSAA

Each gizmo shader now only declares its uniform struct + a 10-line
vertex shader. C++ side gains thickLineVertexLayout(attribs[5]) so
both call sites set up the 5-attribute layout in one call instead of
20+ lines each. Net diff is -28 lines on this commit and roughly -60
relative to the unconsolidated section commit; the next thick-line
gizmo (measure tool, selection outline, …) starts from ~30 lines of
WGSL + a vertex buffer.

Bonsai decorator palette

All overlay colours now come from src/bonsai/bonsai/bim/ui.py's
decorator_color_* defaults so they match Bonsai's Blender add-on:

  decorator_color_error    = (1.000, 0.200, 0.322)  red   → +X axis, section gizmo
  decorator_color_selected = (0.545, 0.863, 0.000)  green → +Y axis
  decorator_color_special  = (0.157, 0.565, 1.000)  blue  → +Z axis

Section gizmo polish

  - Entire gizmo (quad outline + arrow shaft + arrow head) goes red.
    GL's white quad + yellow arrow disappeared against light surfaces;
    one saturated red reads against any background and identifies the
    geometry as a tool overlay.
  - Line width bumped to 5 logical px and the per-vertex tint dropped
    to (1, 1, 1, 1) so the tint multiplier stays available for a future
    "selected" state without changing the base colour.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 16:00:09 +10:00
Dion Moult 8173074050 wgpu: section cutting tool — K hotkey, click-to-add, drag arrow, Esc/Del
Mirrors GL ViewportWindow's section tool end-to-end.

Hotkeys

  K            toggle the tool active
  Shift+K      clearSectionPlanes
  Esc          deactivate the tool
  Del/Bksp     remove the most recently added plane (tool-active only)
  LMB click    pickSurfaceAt → addSectionPlaneAtSurface (no modifier)
  LMB drag    on the arrow gizmo: slide the plane along its normal

State

FrameUniforms grows by clip_count (i32) + clip_planes[6] (vec4). The
six-plane cap matches GL's MaxSectionPlanes. WGSL pads via three
scalar i32s instead of a vec3<i32> so the array starts at offset 144
to match the tightly-packed C++ struct (240 B) — vec3 would have
forced clip_planes to 160 and broken the binding-size match.

is_section_clipped(world) in WGSL evaluates all active planes and
returns true if any signals "on the positive side". Both main and
pick fragments discard with it so cuts are visible AND selection is
consistent — you can't pick something the user can't see.

Surface pick

Pick pass now emits 2 color targets: R32UInt object_id at @location(0)
and RGBA16F packed world-space normal at @location(1). Normal is
packed × 0.5 + 0.5 so unsigned-ish halfs keep the sign. pickSurfaceAt
reads both via 1×1 texel copies (RGBA16F is a color format with no
full-mip restriction, unlike Depth32Float). World position comes from
ray-AABB intersection against the picked instance's AABB — equally
accurate for "drop a plane where I clicked" and dodges the Depth32Float
copy-extent rule entirely. The pick normal is decoded into the
per-fragment surface normal so the plane lands perpendicular to the
actual triangle (not the AABB face).

Plane gizmo

Identical geometry to GL's renderSectionPlanes: 2×2 m quad outline
(white) + 1 m arrow shaft along +n (yellow-orange) + 4 arrow-head
diagonals. Drawn inside the main MSAA pass with depth LessEqual + no
depth write. Lines are rendered as screen-space-expanded thick quads
with fwidth-based AA, same technique the axis indicator uses, so the
gizmo reads against busy BIM geometry rather than disappearing as
1-px hairlines.

Drag

mousePressEvent claims a plain-LMB press if it hits an arrow gizmo
(12 logical-px grab radius, distance to the (origin, origin+n)
screen-space segment). The drag handler projects the cursor delta
onto the screen-space axis and converts to metres via
delta·axis / |axis|² — same formula GL uses. Mid-drag camera moves
keep working because the projection re-runs every frame against the
press-time origin.

Closes the click-to-add + drag halves of #30 / #60.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 15:18:34 +10:00
Dion Moult e6c6df905a wgpu: corner axis gizmo + orbit pivot indicator (shared geometry)
Both overlays draw the same three positive-axis rays (origin → +X / +Y
/ +Z) — one screen-space-thick-line shader, one vertex buffer, one
bind group layout. The vertex stage transforms each vertex as
`mvp * (origin + position * arm)` so the same primitive serves both
modes:

  corner: viewport set to a 110×110 px box in the bottom-left,
          camera-orientation ortho MVP, origin=0, arm=1.
  pivot : full viewport, main view-proj, origin=camera_target,
          arm = 30 logical px in world units.

Pipelines

  axis_pivot_pipeline_      — MSAA + depth LessEqual   (α=1)
  axis_pivot_xray_pipeline_ — MSAA + depth GreaterEqual (α=0.30)
  axis_corner_pipeline_     — resolved surface, no depth, sampleCount=1

Pivot renders inside the main MSAA pass after geometry (depth
interaction); corner renders on the resolved surface after the edge
silhouette pass so the laplacian can't darken its lines. The pivot's
two passes — x-ray first then visible — give an occluded-side hint
matching GL's renderPivotIndicator.

Screen-space thick lines

WebGPU has no lineWidth, so each axis is a 2-triangle quad expanded
by `line_width / 2` pixels along the screen-space perpendicular in
the vertex shader. Every vertex carries BOTH endpoints (start, end)
plus `t ∈ {0,1}` and `side ∈ {-1,+1}` so the direction is computed
consistently as `s_end - s_start` regardless of which end the vertex
sits at — an earlier "this vertex vs the other end" formulation
flipped sign at the end vertex and produced a bowtie.

Analytical AA

|side_t| ∈ [0,1] is the perpendicular distance from the line centre.
`smoothstep(1-fwidth, 1, |side_t|)` gives a 1-pixel coverage falloff
at the long edges — gizmos read cleanly even on the resolved-surface
corner pass which has no MSAA.

Pivot visibility

  - orbit / pan drag press → on, release → off
  - wheel zoom            → on with 600 ms afterglow via QTimer

Pole fallback for the corner gizmo's lookAt mirrors buildViewProj's
identical fix (swap Y-up when |pitch| ≥ 89°), so top/bottom standard
views don't degenerate.

Tracked under task #59.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 14:04:08 +10:00
Dion Moult d4169693c9 wgpu: drop dormant spatial-bucket prototype
Removed the WGPU_SPATIAL_BUCKETS=1 octree-style instance planner
(planSpatialChunks, SpatialPlan, the env-var pair, the field, the
load-time branch). Was an opt-in prototype kept in tree as a possible
acceleration for "find the right instance chunk", but:

- Benchmarked slower than mesh-keyed (~24-26 ms vs ~19.7 ms) — the
  higher chunk count's per-chunk bind-group + draw overhead more than
  ate the tight-AABB win on this dataset.
- Not load-bearing for the brace correctness fix — that turned out to
  be the AABB-projected screen-rect priority metric (commit 6200ab9fa),
  which works on either chunk topology.
- The "find the right chunk" hypothesis is moot: instance_chunk_idx[]
  is precomputed at load time, so cull has nothing to look up.

Git log preserves the implementation if it's ever revisited.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 08:17:14 +10:00
Dion Moult 72af067c9a wgpu: streaming + HiZ correctness fixes
Three correctness bugs found and fixed, plus an unrelated fly-mode
deadlock surfaced along the way.

HiZ false-rejection at the bottom of the screen
------------------------------------------------
The mip-pyramid sizing floored when halving — for a 256×70 mip 0 the
level-3 mip is 32×8, but mip-0 row 69 maps to ly = 69>>3 = 8, which is
out of bounds for an 8-row mip. ly1 then clamps down to 7 while ly0
stays at 8, the sampling loop runs zero times, max_d retains its
initial 0.0, and `min_z > 0` rejects every AABB whose projected y
range touches the bottom row. Same class for the right edge on very
wide viewports.

Fix: ceil rather than floor when halving mip dimensions so every
parent row has a covering child texel, plus std::clamp on both lookup
endpoints as belt-and-suspenders for any future mip-sizing change.

Surfaced after the user added more sidecars and saw "anything near the
bottom of the screen, no matter close or far" disappear ~0.5 s after
camera stops — that delay was the strict-VP gate + readback latency
opening the HiZ window. Found via WGPU_HIZ_TRACE rejection logs that
showed every rejection had `max_d=0` and `ly0 > ly1`.

Streaming priority lets the ocean starve out the bracing
---------------------------------------------------------
Per-instance projected screen footprint was estimated as bounding-
sphere radius squared. BIM geometry is overwhelmingly thin-in-one-axis
(slabs, pipes, columns, windows) and a flat ocean plane viewed nearly
edge-on gets a sphere projection ~250× larger than its actual screen
rect. Its chunk dominated the priority ranking and evicted the brace
chunks despite the braces being one of the closest visible things.

Fix: per-instance priority is now the screen-space AABB rectangle area
(world AABB extents projected onto the camera right/up basis vectors,
divided by view-z). Sphere radius is retained for the contribution
cull and LOD pick because conservative-over is the right failure mode
there.

Stale-VP HiZ gate
-----------------
HiZ resolves into an async ping-pong of staging buffers, so the
pyramid resident at cull time was typically captured one or two
frames ago. During camera motion the captured VP differs from
vp_this_frame and AABBs end up sampling depth taken for what was at
slightly-different screen positions in the old view. Strict by
default now: HiZ engages only when hiz_vp_ == vp_this_frame.
WGPU_HIZ_MOTION=1 trusts the stale pyramid (matches GL's default).

Fly mode Shift+Q deadlock
--------------------------
keyPressEvent requested a redraw only on the first key of a new held
set (was_empty). Pressing Shift first then Q never satisfied that
condition because Shift had already populated the set, so the render
loop never ticked. Now every relevant keypress calls requestUpdate
unconditionally.

HiZ stays opt-in behind WGPU_HIZ=1 for one release while the fix
bakes; WGPU_HIZ_TRACE=1 keeps the per-rejection diagnostic available
for future bugs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 08:06:56 +10:00
carlopav 3f680f5c21 IfcCostSchedule PDF export with typst: fix bugs
Fixed a bug when a summary cost has no sum applied.
Added Currency in table header.
Cleanup.
Added guards for end summary.
2026-05-29 18:38:16 +02:00
Gorgious56 97e8deb069 Cache opening previews + dissolve fill
DecorationsHandler now caches dissolved edges (mesh-keyed), world-space
draw payload, and GPUBatch objects with per-object epoch invalidation —
moving one wall doesn't wipe 50 opening caches. Object-mode dissolve
removes triangulation noise; 2-pass depth-test split dims occluded lines
instead of hiding them. Edit-mode behavior unchanged.

Also: disable viewport shadows for IfcFeatureElementSubtraction objects,
and wire DecorationsHandler.uninstall() into the model module's
unregister() so the new persistent handlers don't leak on addon disable.

Generated with the assistance of an AI coding tool.
2026-05-29 12:16:49 +02:00
Dion Moult 126d2d4c06 wgpu: spatial instance bucketing for streaming (env-gated prototype)
WGPU_SPATIAL_BUCKETS=1 swaps the applyCachedModelStreaming planner from
mesh-keyed Morton+greedy to octree-style instance bucketing. Default
behaviour unchanged (env var unset → mesh-keyed planner runs).

Phase 1 of #55 / #56. The mesh-keyed planner produces chunks whose
AABBs are the union of all instances of the chunk's meshes — for
heavily-deduplicated IFC meshes (a "standard floor tile" used 800
times across a federation) the mesh's "centroid" is a mean of scattered
instance positions and the chunk's AABB ends up spanning the entire
model. Symptom: chunk-level frustum cull rarely fires (AABB always
intersects view), and the screen-area priority metric under-rates
big-AABB chunks because their corners straddle the near plane. Visible
objects pop in/out as the camera tilts, even though they're fully on
screen.

The spatial planner bucketises INSTANCES directly. Each leaf bucket
contains its instance list + the unique mesh data those instances
reference. A mesh whose instances scatter into multiple buckets gets
its vertex/index data uploaded into multiple pool slices — duplication
is the cost for tight bucket AABBs. For IFC this is acceptable:
heavily-shared meshes tend to be small (fittings, fasteners), so
per-bucket duplication adds tens-of-MB not GB.

Octree implementation (planSpatialChunks):
  - work-stack subdivision: for each (instance subset, AABB), split into
    8 octants around centre and recurse
  - stop conditions: bucket fits WGPU_CHUNK_VERTEX_BYTES_LIMIT for
    union vertex bytes AND ≤ spatial_max_instances_ instances; OR single
    instance left; OR every instance falls into the same octant
    (pathological — emit as leaf rather than infinite recurse)
  - spatial_max_instances_ default 5000, overridable via
    WGPU_SPATIAL_BUCKET_MAX_INSTS env var so the prototype can be
    swept without rebuilding

Data-model adjustment beyond what dc2927997 prepared:
  - Per-chunk per-mesh chunk-local offset table (chunk_mesh_offsets)
    built during the chunk-construction loop. The mesh-keyed per-mesh
    global arrays (mesh_chunk_idx etc.) still get populated for
    legacy reads, but under spatial bucketing they're overwritten when
    the same mesh appears in multiple chunks — harmless because cull
    reads the per-instance arrays exclusively (per dc2927997).
  - Post-construction, per-instance arrays are populated from
    chunk_mesh_offsets via (instance_to_chunk[i], inst.mesh_id) lookup.
    Mesh-keyed planner derives identical values to before
    (pixel-identical); spatial planner now writes the correct
    per-bucket offsets even when the mesh appears in multiple chunks.

basic.ifc parity on all three paths confirmed (non-streaming
mesh-keyed, streaming mesh-keyed, streaming spatial all produce 0
pixel diff vs the reference). Spatial planner produced 1 bucket on
basic.ifc (3 instances, well under thresholds) as expected.

Non-streaming applyCachedModel left unchanged — the prototype targets
the streaming path which is where the federation-scale missing-objects
issue lives.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 16:19:56 +10:00
Dion Moult 1f8f6bffc4 wgpu cull: refactor per-mesh chunk lookups to per-instance
Preparatory refactor for spatial instance bucketing (#55). Cull
previously routed through per-mesh tables (mesh_chunk_idx,
mesh_chunk_local_base_vertex, _ebo_first_u32, _lod1_first_u32) to
find the chunk and chunk-local offsets for each instance. That
assumes a mesh lives in EXACTLY ONE chunk — the assumption holds
under the current mesh-keyed planner but breaks under spatial
bucketing, where the same mesh can be duplicated across multiple
buckets if its instances are scattered.

Adds four per-instance arrays (instance_chunk_idx,
instance_base_vertex, instance_ebo_first_u32, instance_lod1_first_u32)
populated at planning time. The current mesh-keyed planner derives
them by translation:
    instance_chunk_idx[i] = mesh_chunk_idx[instances[i].mesh_id]
The spatial-bucket planner (next commit) will populate them directly,
allowing the same mesh_id to map to different chunks for different
instances.

cullModelCpuCompute now reads the per-instance arrays:
- frustum_visible_count uses chunks[instance_chunk_idx[i]]
- VisibleDrawGpu uses instance_base_vertex / instance_ebo_first_u32
  / instance_lod1_first_u32
- LOD-select branch and use_lod1 logic unchanged.

Per-mesh tables stay (used by makeChunkRequest, debug logs, the
planner itself). Memory cost: 16 bytes × N instances ≈ 16 MB on a
1M-instance scene. Pixel-identical on basic.ifc in both non-streaming
and streaming modes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 16:06:58 +10:00
Dion Moult 3eaa35d748 wgpu: input parity, fly mode, diagnostic instrumentation, chunk-priority fix
Brings the wgpu viewport's keyboard + mouse into line with GL ViewportWindow
+ Bonsai's MainWindow shortcut table, lands fly-mode, swaps in three
diagnostic env vars, and fixes a chunk-priority bug exposed by the
diagnostics.

Keyboard parity with GL + Bonsai:
  P             — toggle perspective / ortho projection
  X / Shift+X   — front / back view (eye on ±X, pitch 0)
  Y / Shift+Y   — right / left view (eye on ±Y, pitch 0)
  Z / Shift+Z   — top / bottom view (pitch ±90°)
  F             — focus camera on currently selected object
  Home          — frame entire scene
  C             — print --camera CLI args for current view
  H             — hide selected
  Shift+H       — isolate selected (hide everything not in selection)
  Alt+H         — show all (clear hidden set)
  Shift+F       — enter fly mode (matches BonsaiViewer)
  Escape (fly)  — exit fly mode
  WASD/QE/Shift — fly movement (when in fly mode)

The previous H/Shift+H/I assignments were wrong vs Bonsai (Shift+H went
to show-all, I to isolate); both are fixed.

Fly mode:
  - GL-style absolute m/s base speed (default 5.0), Shift = 5×, scrollwheel
    adjusts ×1.25/×0.8 per notch (Blender convention). Scrollwheel does
    NOT zoom in fly mode; that interfered with speed when speed was
    distance-scaled (it was, briefly; replaced with absolute m/s).
  - Mouse-look pins eye: yaw/pitch update first, then target is re-derived
    so orbitEye(target, dist, new_yaw, new_pitch) == old eye. Result:
    camera rotates in place (FPS) rather than orbiting the pivot.
  - dt ceiling clamp at 100ms (matches GL fps_move_speed_) so a stall
    doesn't warp the camera.
  - Pitch sign matches non-inverted FPS convention (mouse-up = look up).

Mouse-nav presets (WGPU_NAV_PRESET=blender|rhino|revit, default blender):
  Blender — Orbit MMB,        Pan Shift+MMB
  Rhino   — Orbit RMB,        Pan Shift+RMB
  Revit   — Orbit Shift+MMB,  Pan MMB
LMB stays free for selection in every preset. Nav-drag kind is captured
at press time so a mid-drag Shift release doesn't flip orbit↔pan. The
pan up-vector switches to world-Y at near-vertical pitch so panning
still works in top/bottom view (would otherwise NaN at pitch=±90°).

Camera-math refactor:
  buildViewProj(view, proj) centralises perspective↔ortho selection and
  the near-vertical up-vector switch. Four open-coded copies of the
  view/proj build (cull, debug, streaming priority, render uniforms) now
  call it instead, ensuring projection mode + up-vector switch land
  identically everywhere. basic.ifc pixel-diff is 0 (refactor confirmed
  output-equivalent on the path with no ortho / no near-vertical pitch).

WGPU_PRESENT_MODE=fifo|fifo_relaxed|mailbox|immediate (default fifo):
  Diagnostic toggle for stutter analysis. fifo_relaxed gave the tightest
  per-frame dt distribution on a federated bench scene; immediate gave
  uncapped throughput at the cost of tearing. Mailbox not supported on
  Vulkan + NVIDIA Linux but kept as an option for other backends.

WGPU_FLY_DEBUG=1: per-frame [fly] log printing dt, render gap, key
count, speed, position delta. Confirmed render_gap == dt to four
decimal places — fpsIntegrate runs exactly once per render, no
double-tick. Cull cost (~14-20 ms) is the dominant frame variance and
the eventual fix is task #49 (sub-model parallel cull) — fly-mode
stutter on slow scenes is a downstream symptom of cull cost, not a
fly-mode bug.

WGPU_STREAM_DEBUG=1: per-frame [stream-debug] log with cands / enq /
drained / ev_lru / ev_pri / blocked / resident / cycled / max_load.
Confirms thrash / pool-bound / load-budget cases on big scenes.

Pick-and-track diagnostic: clicking an object enumerates every chunk
holding instances of that object (an IFC object can split across
representations / chunks), printing each chunk's AABB + instance AABB +
residency. If any tracked chunk's is_resident flips true → false in
driveStreamingLoads, an EVICTED dump prints with the chunk's AABB,
priority, pool state, this-frame eviction counts, and the top-5
candidates that displaced it. Surfaces exactly why an object disappeared.

chunkScreenAreaPx fix (uses diagnostic to confirm the bug):
  A chunk's AABB is the union of every instance's world AABB in the
  chunk. On a federated IFC the camera commonly sits INSIDE that AABB
  (e.g. inside a 263×30×15 m floor-area bounding box). Previously the
  8-corner projection silently dropped corners with clip.w <= 1e-3
  (behind near plane), so the projected bbox of the surviving in-front
  corners was a tiny fraction of the chunk's true on-screen footprint.
  Result: big-AABB chunks lost every eviction fight, visible objects
  popped out as the camera tilted. Fix: short-circuit to full-viewport
  area when (a) eye is inside the chunk AABB (mirrors GL's
  contributionPasses camera-inside short-circuit), or (b) any AABB
  corner sits behind the near plane (AABB straddles → 8 corners cannot
  honestly measure footprint; conservatively over-prioritise).

The fix is a workaround for the deeper chunking issue — chunks are
mesh-keyed (group of meshes), and a mesh's AABB used in chunking is
the mean of its instances' positions, which is meaningless for
heavily-deduplicated meshes scattered across the scene. The root fix
is task #55 (spatial instance bucketing, runtime prototype) + #56
(sidecar v15 instance-keyed format). chunkScreenAreaPx fix unblocks
the user-visible "missing objects" issue while those land.

Extracted chunkScreenAreaPx from a driveStreamingLoads-local lambda
to a private member so the disappear-diagnostic and any future call
sites can use it consistently.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:23:39 +10:00
Ryan Schultz 6ba5f5af3d Fix CardinalPoint not applied to all selected objects
EditAssignedMaterial propagated layer set usage attributes
to all selected objects but skipped this loop for profile
set usage. Add the same loop so CardinalPoint and
ReferenceExtent are copied to each selected object's
IfcMaterialProfileSetUsage on save.

Generated with the assistance of an AI coding tool.
2026-05-28 21:21:16 -05:00
Dion Moult a63bc63439 wgpu: diagnostic instrumentation for cull/streaming perf
Adds three knobs and three new heartbeat numbers to support the
ongoing perf-parity work. None affect behaviour in default runs.

cull[wall|compute|upload] split timer
  The existing cull_timer wrapped both the parallel std::async dispatch
  and the sequential cullModelCpuUpload loop (queueWriteBuffer × 3 per
  resident chunk × ~120 chunks ≈ 360 wgpu calls per frame). Splitting
  them ruled upload out as the bottleneck on a 51-model federation
  scene: compute ≈ 16-17 ms, upload ≈ 1 ms.

WGPU_CULL_THREADS=0 — force sequential cull
  std::async-per-model was already in place; this env var disables it
  so we can compare wall time vs sequential and confirm parallelism is
  working. On the federation scene with 52 models: sequential 74 ms vs
  parallel 17 ms = 4.4× speedup. Confirmed; the 17 ms floor is not a
  parallelism failure, it's the cost of culling the largest single
  model (model 43, 114k instances) ÷ no parallelism within that model.

WGPU_STREAM_DEBUG=1 — per-frame [stream-debug] log
  Surfaces cands/enq/drained/ev_lru/ev_pri/blocked/resident/cycled/
  max_load each frame from driveStreamingLoads. The "cycled" /
  "max_load" pair makes thrash vs eviction-churn vs just-loading
  distinguishable. Off by default; opt-in via the env var.

Bench-warm timeout dump
  When [bench warm] times out (600 frames without 0-loads streak),
  prints a structured summary: resident/missing/total chunks,
  cycled count, pool usage, largest free run, avg missing chunk
  size, and an auto-classifier diagnosis (POOL FRAGMENTED vs
  WORKING SET > POOL vs FEW-CHUNK CYCLE vs still-loading). Caught
  a real fragmentation pattern (18 MB largest free run vs ~100 MB
  typical chunk) on a 51-model run where the dumb classifier
  would have called it a load-budget problem.

LOD1 firing counter
  "lod1 X/Y (saved Z tris, N no-lod1)" suffix on the [frame] log.
  X = LOD1-selected this frame, Y = LOD1-eligible, Z = tris not
  drawn vs always-LOD0, N = visible instances with no baked LOD1
  (mesh below IFC_LOD_MIN_TRIS). Confirmed LOD1 path is genuinely
  firing post the per-chunk LOD1-storage commit, and exposed that
  ~90% of instances in real scenes are no-lod1 meshes — relevant
  to the future LOD-tier-residency design.

Chunk.load_count + Chunk.lod0/1 layout bookkeeping
  Per-chunk reload counter for the thrash detector. lod0/1
  layout_count fields prep the data model for distance-tiered
  residency (Phase B of #31) but aren't acted on yet.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 12:17:03 +10:00
Dion Moult 6a3dd4a0eb wgpu cull: contribution-cull defaults 2/10 → 3/15, env-var overrides
wgpu computes projected_px from view-Z distance (forward·(centre-eye)),
which is the perspective-divide-correct denominator: an instance's
on-screen radius really is world_radius * focal / z_view. GL computes
the same value but with euclidean distance (sqrt(dx²+dy²+dz²)) — for
off-axis instances euclidean > z_view, so GL underestimates screen
size and culls more aggressively at the same numeric threshold.

Concretely on the federation scene at the test camera, wgpu was
drawing ~3× the instances GL drew despite identical 2/10 thresholds:
wgpu obj 11067, hiz_rej 16532 vs GL obj 2310, hiz_rej 6823. Same
fps (vsync-pinned), but ~30% more cull work for raster output that
the user already wasn't seeing because GL had been quietly dropping it.

Keeping wgpu's view-Z formula (more physically correct) and bumping
the thresholds to 3.0 / 15.0 to match GL's effective drop rate. On
the federation scene this lands obj/tri counts within ~10% of GL
across the orbit, and shaves cull from 3.44 ms to 2.61 ms avg.
basic.ifc parity is unchanged (its 3 instances clear 3 px easily).

WGPU_MIN_PX / WGPU_MIN_PX_MOTION env vars added so the thresholds
can be swept without rebuilding — needed while we visually
confirm the new defaults across more scenes / cameras.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 09:40:18 +10:00
Dion Moult 10f8fc88e8 wgpu chunks: pack LOD1 indices alongside LOD0; cull picks per-instance
Sidecar LOD1 is per-mesh, index-only — meshoptimizer-baked decimated
index slices that share the LOD0 VBO. Before this change the wgpu
chunk path force-disabled LOD1 (effective_lod1 = false; tagged in a
comment as "until per-chunk LOD1 storage lands"), so it had to walk
every visible instance at full LOD0 even when the per-instance LOD
selection said the projected radius was below the LOD1 threshold.

Per-chunk layout: append LOD1 indices for the chunk's meshes after
all LOD0 indices in the same pool slice. A new
mesh_chunk_local_lod1_first_u32 array records each mesh's LOD1
starting offset (in u32s) within the chunk's index slice; LOD0
offsets stay where they were. The cull's emit then sets
VisibleDrawGpu.ebo_first_u32 to whichever side matches the per-
instance use_lod1 decision the prior #8 commit already computed.
Vertex pulling is oblivious to the LOD split — it just reads the
indices the cull pointed it at.

c.index_count is repurposed as the LOD0+LOD1 total so the pool
allocation, eviction's pool-fit check, and the VRAM accounting all
scale automatically. c.lod1_index_count exposes the LOD1 portion
for stats.

makeChunkRequest appends LOD1 byte ranges to req.i_ranges in the
same per-mesh order; the streaming worker concatenates ranges in
order, so the assembled idx blob lands LOD0-first / LOD1-second
which matches the chunk-local packing.

On a 10-model regen with meshoptimizer-baked sidecars, the bench
[frame] heartbeat shows lod1 firing on ~80-90% of LOD1-eligible
instances and saving 10-30M tris per frame versus the prior
LOD0-only ceiling. Same camera/scene on basic.ifc is still
pixel-identical (no mesh in basic.ifc is large enough to bake a
LOD1, so the cull just follows the LOD0 path it always did).

Temporary debug counters (lod1_dbg_count_ et al.) print
"lod1 X/Y (saved Z tris, N no-lod1)" in both interactive and
benchmark [frame] heartbeats — kept on while LOD1 correctness gets
confirmed across more scenes, will come out once trust is built.

The non-streaming applyCachedModel path runs the same LOD1 plumbing
but no longer fits the full federation scene in pool (extra index
bytes push past the 2 GB single-buffer cap); that mode was
already streaming-only on that scene before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 09:02:27 +10:00
Ryan Schultz 335ee1a1bb Fix negative zero in imperial feet-inches parser
When the user enters `-0' - 10"`, Python parses feet as -0.0.
The check `feet < 0` is False for negative zero, so the sign was
silently dropped. Use math.copysign to detect it correctly.

Generated with the assistance of an AI coding tool.
2026-05-28 12:14:23 -05:00
Gorgious56 49348908e6 Add wall-fillet helper functions + recreate_wall hook
Eleven module-level helpers in wall.py that the upcoming wall-fillet
operators + gizmo groups depend on. Each is self-contained or
references only helpers earlier in the file; the operators and
gizmos themselves land in follow-up commits.

* _wall_fillet_props / _wall_fillet_preview_active /
  _wall_fillet_preview_walls: thin read-side accessors over the
  BIMPreviewProperties.wall_fillet pointer (added with the
  operators commit). Safe today: get_preview_props returns None
  until the pointer is attached.
* _walls_have_zero_slope_for_fillet: validates that input walls
  are vertical (x_angle ~ 0); slanted-extrusion fillets require
  swept-along-curve geometry the banana profile builder doesn't
  support.
* _build_curved_corner_body_representation: builds the banana
  (annular sector) IfcExtrudedAreaSolid as a polyline-tessellated
  IfcIndexedPolyCurve.
* _apply_fillet_corner_geometry: positions the corner wall at
  tangent_a and rebuilds its body. Shared by the creation operator
  and the regenerate path.
* _resolve_two_walls: pulls (active, other) from a 2-wall
  selection, validates both as LAYER2 + straight-axis + not-already-
  a-fillet-corner.
* _pick_dominant_wall_material: returns the thickest layer's
  material from an element's IfcMaterialLayerSet / Usage.
* regenerate_fillet_corner_wall: re-runs the geometry build from
  BBIM_Wall.FilletRadius + current neighbour layer parameters.
  Called by tool.Model.recreate_wall when the IsFilletCorner pset
  is set; the FIXME(PR4) placeholder in recreate_wall is dropped.
* _wall_fillet_gizmo_x_matrix: 4x4 placement matrix with local +X
  aligned to a world-space direction; used by the fillet preview
  gizmo group.

Centralises the IsFilletCorner pset read as
tool.Parametric.is_fillet_corner_wall — replaces 3 inline
get_pset(element, "BBIM_Wall", "IsFilletCorner") sites
(tool.Model.recreate_wall, tool.Model.recalculate_walls,
tool.Parametric.is_path_connectable_wall) plus the new
_resolve_two_walls call.

Generated with the assistance of an AI coding tool.
2026-05-28 16:42:42 +02:00
Gorgious56 c250b2c1a7 Gate parametric-edit array gizmo until integration completes
The framework's parametric-edit icon row currently binds an array
icon to bim.add_array_from_feature_edit, but the supporting per-
feature add-array flow and gizmo positioning haven't fully landed.
Showing the icon today lets the user click it and trigger a half-
wired flow.

Force the icon hidden inside the props.is_editing branch of
BaseParametricGizmoGroup.update_editing_gizmos. The else-branch
(not editing) already hides it, so this just mirrors that behavior
during edit mode. Drop this gate when array integration completes
to re-enable the icon position + visibility plumbing.

Generated with the assistance of an AI coding tool.
2026-05-28 15:30:46 +02:00
Gorgious56 6874d52100 Add cursor-aware extend-arrow flip on wall edit gizmos
The extend-X / extend-Z icons in GizmoWallEdition's cursor row are
billboarded toward the camera; without orientation polish they
always point in the same screen-space direction regardless of which
wall endpoint the click will move (or whether the cursor sits above
or below the wall top). New helper mirrors the icon's local-X (extend-X)
or local-Y (extend-Z) axis so each arrow points toward the end it
will move:

* Extend-X: walk wall midpoint to figure out which endpoint stays
  fixed (cursor past midpoint → ATSTART stays; cursor before midpoint
  → ATEND stays). Project the fixed endpoint into screen-space and
  flip the arrow when the gizmo's anchor sits on the same side.
* Extend-Z: flip when the cursor is below the wall top (within
  EXTEND_FLIP_EPSILON tolerance).

Called once per resolved cursor gizmo from
``GizmoWallEdition._update_cursor_gizmos``, after the gizmo's
``matrix_basis`` is set by ``gizmo.billboarded_at``. Reuses
``gizmo.should_flip_extend_arrow`` + ``EXTEND_FLIP_MIRROR_X/Y`` +
``EXTEND_FLIP_EPSILON`` already on tool.

Generated with the assistance of an AI coding tool.
2026-05-28 15:14:51 +02:00
Gorgious56 6c21e2b6f4 Add single-wall unjoin operator + gizmo group
GizmoWallJoinIntersection's unjoin only fires when exactly two walls
are selected and surfaces one icon at their shared corner — useless
when the wall has 3+ joins and the user wants to disconnect just one.

* UnjoinWallPathConnection: surgical counterpart to UnjoinWalls.
  Disconnects the active wall from a single partner wall identified
  by IFC GlobalId (invariant under Blender-object renames + file
  save/reload + undo). Walks both inverse arrays of the active wall
  for the specific IfcRelConnectsPathElements joining the pair —
  matches DumbWallJoiner.split's pattern and avoids disconnect_path's
  direction-sensitivity. Resyncs both walls' draft props after the
  recreate_wall pass.
* GizmoWallUnjoinSingle: activates on exactly-one selected
  LAYER2 wall. Preallocates a pool of 16 unjoin icons (Blender forbids
  gizmo allocation outside setup(); ATSTART + ATEND + ATPATH rels are
  rarely more than a handful). Per-frame, iterates _iter_path_connections,
  positions one billboarded icon at each join via
  tool.Wall.path_connection_location_world, and hides the rest. Each
  visible icon's bound operator carries the partner GlobalId, so a
  click removes only that one rel.
* model/__init__.py: register both classes alphabetically.

Mutually exclusive with GizmoWallJoinIntersection via poll() — that
group requires len(selected) == 2; this one requires 1.

Generated with the assistance of an AI coding tool.
2026-05-28 15:05:23 +02:00
Gorgious56 70845e4dd4 Add wall path-connection inverse-walk helpers
The single-wall unjoin gizmo needs to enumerate every
IfcRelConnectsPathElements a wall participates in, regardless of which
side of the rel the wall was authored on, and place an icon at each
join's physical location. Two helpers carry that work:

_path_connection_location_world wraps core.compute_path_connection_location
at the Vector boundary. _iter_path_connections walks ConnectedTo +
ConnectedFrom, normalises orientation to (other, self_ct, other_ct),
and filters non-wall partners + None refs so per-frame gizmo positioning
survives malformed IFC.

Generated with the assistance of an AI coding tool.
2026-05-28 14:23:35 +02:00
Dion Moult c7fba1abb8 wgpu streaming: screen-space AABB priority + grace period + interactive heartbeat
The chunk priority metric is now the 2D projected pixel area of the
chunk's AABB on screen — 8 corners projected through view-projection,
2D axis-aligned bbox of the projected points, clamped to viewport.
This replaces the prior bounding-sphere-radius² metric, which was a
3D approximation: it treated a 322 × 55 × 5 m slab as a 163 m sphere,
giving it the same huge priority face-on or edge-on. The new metric
genuinely answers "what would this chunk's AABB cover if rendered
solid given the current camera and viewport."

Newly-loaded chunks get a 30-frame grace period at full priority
(visibility_history floor temporarily forced to 1.0). Without it,
just-loaded chunks crashed to history=0 → effective priority = pri ×
0.05 → immediately reverse-swapped by the chunk they displaced.
Cycle starved the per-frame load budget so candidates ranked below
the cyclers never got attempted. 30 frames = HISTORY_ALPHA's time
constant — enough for visibility_history to develop meaningfully.

EVICT_PRIORITY_RATIO bumped 1.21 → 2.0 to suppress more swap noise
between similar-priority chunks.

Interactive heartbeat log added: every render in non-bench mode prints
[frame] with fps, ms, obj, sub_draws, hiz_rej, cull, stream, chunks
breakdown (resident/frustum/total + missing count), VRAM, model count.
Every 30 frames when something's missing, also dumps:
- top 8 models by missing chunk count
- top 20 missing chunks by priority (with AABBs)
- bottom 5 residents by effective priority
- all chunks of brace.ifc (one-off diagnostic, hardcoded
  for the brace-visibility investigation)

The heartbeat made the streaming bug visible: a brace model that
isolation-loads correctly is missing in the full set because slabs
covering more pixels win the priority contest. Per-model fairness or
manual pinning are the remaining options if pixel-area + grace +
hysteresis isn't enough — left for follow-up so the user can decide
based on real testing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 22:19:24 +10:00
Gorgious56 d7b5ac1453 Add wall draft-resync helper + wire 6 mutation operators
After a one-shot wall IFC mutation (unjoin / split / merge / extend /
join-at-corner …) the always-visible gizmos on the OTHER side of the
join can be left reading stale ``BIMWallProperties`` — the IFC
geometry moved but the draft props that drive the gizmo handles still
point at the pre-mutation numbers, so a subsequent edit-mode enter
shows the wall at its old length / position.

* New ``_maybe_resync_wall_props_from_ifc(obj)``: re-primes a single
  wall's draft props from current IFC, with guards for non-walls,
  non-parametric walls, and walls in an active draft session (the
  draft is then the source of truth, not IFC). Must run from an
  operator ``_execute`` — ID writes from gizmo refresh raise.
* New ``_resync_walls_after_mutation(objs)``: iterates the above
  across a selection.
* Six existing mutation operators gain a resync call after their
  ``core.*`` / ``DumbWallJoiner`` mutation completes:
  UnjoinWalls, ExtendWallsToUnderside, ExtendWallsToWall, SplitWall,
  MergeWall, JoinWallsIntersection. MergeWall resyncs only the
  surviving wall — the active wall is the deletion target.

Generated with the assistance of an AI coding tool.
2026-05-28 14:06:11 +02:00
Gorgious56 1961cd905e Fix parametric framework live-session regressions
Bundle of bugs surfaced when exercising the new gizmo framework
end-to-end in a live Blender session after the
bim/module/drawing/gizmos.py refactor + TypeAccessor/CycleType/PickType
mixins landed.

Register / annotation resolution
* parametric_lifecycle.py: hoist `entity_instance` import out of
  TYPE_CHECKING so typing.get_type_hints resolves the
  Callable[[entity_instance], bool] annotation at operator registration
  (CycleDoorType, CycleWindowType, CycleStairType failed with NameError).
  Clarify the INTERFACE return contract on the picker entry-point so
  readers see why the gizmo step stays off the undo stack.

Framework callable contracts
* model/wall.py, door.py, window.py, stair.py: migrate `props_getter`
  and `element_checker` from bl_idname strings to bound classmethods
  on tool.Model / tool.Parametric. BaseParametricGizmoGroup.get_props
  expects a callable; the string form raised TypeError on first
  gizmo poll.
* model/door.py, model/stair.py: drop the dead `prop_path=` operator
  kwarg from create_arc_gizmo / create_icon_gizmo call sites. The
  framework helper blindly setattrs every kwarg onto the operator's
  OperatorProperties, but ToggleDoorSwing / ToggleStairProperty don't
  declare prop_path — the setattr raised mid-setup_element_specific_gizmos,
  so self.gizmo_door_type / self.lock_gizmo never got assigned and
  every subsequent draw_prepare tornadoed AttributeError. Nothing
  reads op.prop_path anywhere; the kwarg was dead data.

Dispatcher operators
* model/array.py: add EnableEditingParametric (the framework pen-icon
  dispatcher that routes to a per-feature edit operator by bl_idname
  string) and AddArrayFromFeatureEdit (binds the framework's array
  icon to bim.add_array on the current parametric draft).
* model/__init__.py: register both new operators.

Per-frame robustness
* drawing/gizmos.py: guard BaseParametricGizmoGroup.draw_prepare with
  is_setup_complete() — matches the existing guard in refresh() and
  in BaseSchematicGizmoGroup.draw_prepare(). Defense-in-depth: when
  any subclass's setup raises mid-way, draw_prepare now no-ops cleanly
  instead of per-frame AttributeError-tornadoing on whatever attribute
  the failed setup phase was meant to populate.
* model/decorator.py: guard ProfileDecorator.__call__ against
  context.active_object is None. The decorator is a per-frame
  viewport draw handler; deselecting or deleting the active object
  while it's installed crashed on obj.mode access. Treat None the
  same as "no longer in edit mode" — uninstall + fire the exit
  callback if present.
* geometry/data.py: ViewportData.load() populates `data` before
  flipping `is_loaded`, so a raise from cls.mode() no longer leaves
  the class flag-set but data-empty for subsequent reads.

Generated with the assistance of an AI coding tool.
2026-05-28 13:48:15 +02:00
Dion Moult 3d368e0079 wgpu chunks: 3D Morton-code spatial sort (tight voxel chunks)
The previous chunk-plan sorted meshes by lexicographic (z, y, x)
centroid — effectively a 1D Z-slab traversal. On a typical IFC
building (50 × 50 × 100 m), a 16-MB chunk's 80-ish meshes spanned
roughly 50 × 50 × 0.5 m. On a city federation it was much worse:
the first chunk grouped ground-floor stuff from every building,
spanning the entire scene horizontally. Per-chunk AABBs that wide
make frustum / contribution / HiZ rejection useless (every chunk
"overlaps the frustum" by virtue of spanning the whole scene).

3D Morton (Z-order) interleaves bits of quantised (x, y, z)
centroids, so consecutive items in the sorted order cluster in all
3 axes — chunks become tight 3D voxels of the model. Prerequisite
for the contribution-aware eviction priority (task #25) to actually
discriminate near and far chunks.

21 bits per axis = ~2 M bins per axis, sub-millimetre precision on
a kilometre-scale scene. Both apply paths (streaming and non-
streaming) share the same sortMeshIdsByMorton helper.

Benchmark unchanged (~47 fps avg, 20 ms cull, 0.3 ms stream) — the
distance-based evictor still keys on chunk centres, which moved
slightly under Morton but not enough to materially shift residency.
The user-visible win comes from the next commit, which switches
priority to screen-space contribution × HiZ history — both of which
need today's tight AABBs to mean anything.

Pixel-identical to non-streaming on basic.ifc on both paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 18:04:05 +10:00
Dion Moult 0ec72482c2 wgpu pool: halve-on-failure in addSubBuffer extracts +35% VRAM
Many Vulkan drivers cap a single VkDeviceMemory allocation at exactly
maxStorageBufferBindingSize (NVIDIA: 2 GB on consumer GeForce) or
refuse big contiguous allocations once heap is fragmented. The old
addSubBuffer gave up at the first refusal, latching growth_disabled_
— so on a 4 GB GeForce we extracted 2 GB and called it done.

The wgpu-mem-probe tool (feab05650) showed the driver actually grants
~3 GB total across multiple sub-buffers — invariant under allocation
pattern (2+1+small, 3×1 GB, 6×512 MB, 12×256 MB all land at 3 GB).
The cap is the hardware/desktop, not the request size.

addSubBuffer now starts at last_growth_size_ (initially
per_sub_buffer_capacity_, decays as the driver refuses larger sizes)
and halves on failure inside a single call. Stops at a 64 MB floor;
below that the per-sub-buffer bookkeeping cost (free list, bind
groups) isn't worth it. growth_disabled_ now latches only when even
64 MB is refused — a true hardware ceiling, not just "the first
attempt didn't fit."

pool_can_fit gains a next_growth_size_bytes() accessor to stay
honest about how big a future sub-buffer can be after the driver
has refused larger sizes.

Measured (big federation, --streaming, close camera):
  pool capacity:  2048 MB → 2688 MB (2 GB + 512 MB + 128 MB)
  VRAM resident:  2155 MB → 2800 MB (whole scene fits, no eviction)
  avg fps:        42 → 53
  stream time:    2.5 ms → 0.1 ms (no churn — working set is stable)

On larger GPUs (8 / 16 / 24 GB workstations) the same code extracts
proportionally more (e.g. 4 × 2 GB on a 10 GB+ card).

The GL backend's higher "4+ GB resident" claim is overcommit into
host RAM — explicit Vulkan/wgpu memory management deliberately
doesn't paper over that, and the wgpu-mem-probe data confirms it
isn't recoverable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:31:31 +10:00
Dion Moult feab05650d wgpu-mem-probe: standalone tool to investigate driver VRAM ceilings
New headless wgpu probe app — no Qt, no surface, just initializes a
device and stress-tests buffer allocations. Reports:
1. Adapter + device limits (maxBufferSize, maxStorageBufferBindingSize).
2. Single-allocation probe: descending sizes, each released, finds
   the largest single buffer the driver will grant.
3. Cumulative probe: halve-on-failure, finds total VRAM the runtime
   will let us park behind one device across multiple sub-buffers.
4. Fixed-size cumulative probe: 1 GB / 512 MB / 256 MB uniform sizes,
   to detect whether the "big-first" strategy leaves VRAM on the table.

Findings on a GTX 1650 (4 GB physical) + wgpu-native + Vulkan:
- maxStorageBufferBindingSize = 2 GB (driver cap, not wgpu-native).
- Any single storage buffer > 2 GB is REFUSED.
- Total available across N sub-buffers = ~3 GB, INVARIANT under
  allocation pattern (2+1+0.06, 3×1 GB, 6×512 MB, 12×256 MB all
  reach 3.00 GB). Driver hands out a fixed VRAM slice; pattern
  doesn't matter.
- Remaining ~1 GB is held by the desktop compositor + OS.
- GL's higher "4 GB+ resident" claim is overcommit into host RAM,
  which wgpu/Vulkan don't do.

The +50% (2 → 3 GB) improvement is real and worth chasing — a
follow-up halve-on-failure addSubBuffer in WgpuBufferPool will
extract that on this card. On 8/16/24 GB GPUs the same code gets
us proportionally more.

Build: ninja -C build-viewer-wgpu WgpuMemProbe
Run:   ./build-viewer-wgpu/wgpu-mem-probe/WgpuMemProbe

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:22:57 +10:00
Dion Moult dcc2bf1c01 wgpu streaming: background-thread chunk I/O kills render-thread stutters
The sync chunk-read on the render thread was causing 100-300 ms spikes
during orbit whenever a new chunk needed to scatter-gather its mesh
bytes from disk. p99 was 326 ms on the close-camera benchmark.

New WgpuStreamingThread: one worker thread with a condvar-protected
request/result queue. driveStreamingLoads becomes drain-then-enqueue:
1. Drain any results the worker pushed since last frame. For each,
   pool-allocate slices + queueWriteBuffer + build the chunk bind
   group (still main-thread because wgpu queue ops aren't thread-safe).
2. Walk visible non-resident chunks (sorted by distance), evict to
   make pool room, and enqueue the request. Chunk gains is_loading
   flag to prevent re-enqueueing while in flight.

loadChunkBytesAndUploadGpu becomes the sync fallback path, used only
when a screenshot is pending — the deferred-capture wait would
otherwise let the window manager re-layout the window between frames
and the test framework would capture at the wrong size. Normal
streaming always goes through the worker.

Bench warm-gate / requestUpdate gating updated to consider
streaming_thread_.inFlightApprox() so we don't declare "converged"
while a worker read is still in flight, and the render loop stays
alive until the worker queue is empty.

Refactored loadChunkBytesAndUploadGpu into two helpers:
- makeChunkRequest: builds the worker request from chunk metadata
- applyStreamedChunk: pool.alloc + queueWriteBuffer + bind group
Both the sync and async paths share applyStreamedChunk.

Benchmark (big federation, --streaming):
  close camera:    avg 24 fps p99 47 ms (was 27/326)
  default camera:  avg 24 fps p99 46 ms (was 31/186)
  stream time:     ~2 ms (was 8-12)
  cull is now the bottleneck (20 ms median) — task #17 (GPU compute
  cull) is the next frontier.

Pixel-identical to non-streaming on basic.ifc on both paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:37:18 +10:00
Dion Moult 6f66d08bee wgpu cull: chunk-level frustum cull replaces BVH walk
cullModelCpuCompute previously had two paths: a flat linear scan over
all instances (default), or a BVH-stack walk (--bvh, gated off because
it regressed on dense scenes — the BVH built per instance but its
interior-node AABBs spanned huge chunks of model so most subtrees
straddled the frustum and the walk overhead beat the rejection win).

With spatial chunk planning (commit 4d3617420) chunks ARE already a
one-level spatial partition of the model, with tight per-chunk AABBs.
So the same wholesale-reject behaviour falls out of just walking
m.chunks: frustum-test each chunk's AABB once, and on hit, iterate
its (new) instance_ids list. No per-node traversal overhead, no
dependency on rebuilding a BVH alongside the chunk plan.

Changes:
- Chunk gains an instance_ids vector, populated in both apply paths
  alongside the per-chunk AABB accumulation.
- cullModelCpuCompute drops the if-bvh / else-linear-scan dichotomy
  in favour of `for chunk: frustum-test then iterate c.instance_ids`.
- Per-model ModelBvh field, buildModelBvhOne call sites, BvhAccel.cpp
  in CMakeLists, bvh_enabled_ field, and --bvh CLI flag all removed —
  dead code now that chunk-cull subsumes them.
- BvhAccel.{h,cpp} stay in src/ifcviewer for the GL backend's use.

Benchmark (big federation, --streaming, close camera): avg 37 fps
(was 36) / median 53 (was 53). Same order on the metric — the
parallelism across models was already amortising frustum-check cost,
so the per-chunk early-out saves only fragments of cull wall time.
Real cull-perf win will come from chunk-level HiZ (potentially) or
GPU compute cull (task #17). What this commit really delivers is
architectural simplification + removal of a dead-but-not-dropped
code path.

Pixel-identical to non-streaming on basic.ifc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:20:30 +10:00
Dion Moult 4d36174200 wgpu streaming: spatial chunk planning + coalesced multi-range reads
Chunks are now grouped by world-space centroid instead of mesh-id
range, so each chunk's AABB tightly bounds its geometry instead of
spanning the whole model. Distance-based eviction can finally
distinguish the near corner of a skyscraper from the far corner.

Algorithm:
1. Compute each mesh's centroid = mean of its instances' world AABB
   centres.
2. Sort mesh indices lexicographically by (z, y, x) centroid. Stable
   sort keeps mesh-id order as tiebreaker for instanced repeats.
3. Greedy-pack sorted meshes into chunks ≤ WGPU_CHUNK_VERTEX_BYTES_LIMIT.
4. Each Chunk stores its mesh_ids list; the per-mesh layout (chunk_local
   base_vertex / ebo_first_u32) is computed by walking the list at plan
   time.

Loader: chunk vertex/index bytes are no longer file-contiguous, so
streaming uses new multi-range read paths
(readSidecarVertexRanges / readSidecarIndexRanges). Each range list
is sorted by file offset and adjacent ranges coalesced with a 64 KB
gap tolerance — on the close-camera benchmark this brings the
per-chunk seek count back down to ~mesh-id-grouping levels, so the
spatial sort costs ~nothing on I/O while delivering tighter AABBs.

Non-streaming applyCachedModel mirrors the spatial plan but gathers
from in-memory data.vertices / data.indices via per-mesh
queueWriteBuffer calls at chunk-local offsets.

Chunk struct drops vertex_byte_offset and index_first_u32 (no longer
meaningful — each chunk is N scattered ranges). vertex_byte_size and
index_count stay as aggregates for pool sizing + eviction math.

Tuning: kept WGPU_CHUNK_VERTEX_BYTES_LIMIT at 128 MB. Tried 8 MB and
32 MB; both gave tighter AABBs but the scatter-gather I/O cost blew
up because the per-frame load count grows linearly as chunks shrink
(orbit shifts the working set faster across finer chunks). 128 MB +
coalescing is the empirical sweet spot pre-v14. Once sidecar v14
re-orders bytes on disk to match spatial chunks, we can drop the
limit to ~8 MB for sharp eviction without re-paying the seek cost.

Benchmarks (big federation, --streaming):
  close camera:     avg 36 fps median 53 (was 35/49) — parity
  default camera:   avg 33 fps median 47 (was 40/49) — small regression
                    likely from increased coalesce overhead on more-
                    scattered orbit traversals; will resolve with v14.

Pixel-identical to non-streaming on basic.ifc on both paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:45:15 +10:00
Dion Moult c3a55d7f7b wgpu streaming: multi-pool growth, frustum-only residency, sorted convergence
Five interlocking fixes that take --streaming on the big federation
scene from "5 fps + endless flicker + infinite cold-load" to a
stable 35-49 fps with a converged working set.

1. Multi-sub-buffer WgpuBufferPool. Pool now grows lazily by adding
   sub-buffers of per_sub_buffer_capacity_ when alloc demand exceeds
   existing free runs. Each Slice carries (buffer, offset, size,
   sub_idx). On driver refusal of addSubBuffer, growth_disabled_
   latches so subsequent allocs don't keep retrying and log-spamming.
   pool_can_fit consults can_grow() to know when growth could rescue
   a candidate vs when eviction is the only path.

2. Split cull / stream benchmark timers. The previous "cull[wall]"
   metric was actually cull + driveStreamingLoads, blaming the wrong
   subsystem (~170 ms of "cull" was synchronous disk I/O).

3. frustum_visible_count on Chunk, populated in cullModelCpuCompute
   right after the per-instance aabbInFrustum check. driveStreamingLoads
   now keys residency on this instead of total_visible_draws (which
   includes contribution + HiZ). HiZ visibility flips frame-to-frame
   as occluders shift; using it for residency caused chunks to be
   evicted then immediately re-loaded, every frame, even with a
   stationary camera — both the perf cliff and the visible flicker.

4. Distance-sorted candidates in driveStreamingLoads. Walk the
   non-resident frustum-visible chunks in distance order (closest
   first). With sorted processing, evict_farthest_than converges
   monotonically: each swap replaces a far resident with a closer
   candidate; once the next candidate is farther than every
   remaining resident, the loop exits. Without sorting the loader
   visited candidates in model/chunk-id order, swapping random
   chunks every frame without ever converging.

5. 10% eviction hysteresis (EVICT_DIST2_RATIO = 1.21). On scenes
   where many chunks are clustered at similar distance from the
   camera (e.g. several chunks all ~370 m away), naive
   "evict any resident strictly farther than candidate" triggers
   sub-meter swaps every frame, never resting. Requiring the victim
   to be 10% farther in linear distance kills these cycles while
   still allowing genuine "much closer" candidates to evict.

Plus: latched bench_warm_done_ on the cold-load gate, with a
5-frames-of-zero-loads convergence test (default-camera big scene
converges in 20 frames) and a 600-frame timeout fallback that prints
exactly once.

Measured on the test federation (111 sidecars, ~3 GB raw, 1 M
instances) with the user's close-in camera:
- avg 35 fps (was 5), median 49 fps (was 7)
- cull 19 ms (now the bottleneck), stream 5-8 ms (was 172)
- p99 184 ms — occasional big-chunk load on the render thread;
  background-thread I/O would smooth that out as a follow-up.

With the default wide camera:
- avg 40 fps, converges in 20 frames, residency grows naturally
  from 59 → 76 chunks as orbit shifts the frustum.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:08:16 +10:00
Dion Moult 502c29fbc2 wgpu: probed-size pool replaces per-chunk createBuffer
Drops the per-machine "guess the OOM ceiling" budget knob in favour of
a single buffer pool whose capacity is *probed* at device-init time.
The runtime answers the question: descend from min(maxBufferSize, 4 GB)
through OOM error scopes, accept the largest size that allocates
cleanly. On a desktop wgpu-native v29 box this lands at 2 GB; on
browser-class platforms it'll land at 256 MB – 1 GB depending on the
implementation. Same code path either way.

Architecture:
- WgpuBufferPool (new): single WGPUBuffer + free-list sub-allocator
  with adjacent-range coalescing and first-fit. 256 B alignment for
  storage-binding offsets.
- Chunks now hold (pool_vertex_offset, pool_vertex_size) and
  (pool_index_offset, pool_index_size) instead of per-chunk WGPUBuffer
  handles. Load = pool.alloc + queueWriteBuffer. Unload = pool.free.
- Bind groups bind pool_.buffer() at the chunk's specific (offset, size)
  for both the vertex and index storage bindings.
- Eviction queries pool.largest_free_run_bytes() instead of a tracked
  budget; the two-phase LRU/distance evictor's policy is unchanged.

What this fixes:
- No more gpu-alloc-rs fragmentation OOM: one VkDeviceMemory block
  instead of N per-chunk blocks with rounding overhead. On the test
  dataset (~3 GB on disk, 562 k visible instances) the wgpu backend
  now runs through to render without OOM at any point.
- No --streaming-vram-mb knob, no hardcoded budget constant, no
  per-machine calibration. The pool size adapts to whatever the
  runtime grants.

Notes:
- Error scope probing: wgpu-native v29 classifies "Not enough memory
  left" as WGPUErrorType_Validation, not OutOfMemory. We push both
  filters (nested) and treat either firing as probe failure.
- The 4 GB probe cap is principled, not magic: above that, wgpu-native's
  advertised maxBufferSize is sometimes a sentinel (1 TB) that just
  forces wasteful halving steps. 4 GB is the largest buffer any
  realistic WebGPU implementation will grant a single allocation today.
- Pool destroy()/release happens after model release in shutdown() so
  the underlying buffer outlives every bind group that references it.

Follow-ups: spatial chunking (task #22) for finer eviction granularity;
cull perf needs work at 100+ models / 1M+ instances (separate from
streaming concerns).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:21:52 +10:00
Dion Moult 71e61dd8a5 wgpu streaming (5/4): per-chunk indices + LRU/distance eviction (stopgap)
Defers index buffers per-chunk (alongside vertex bytes) so streaming
fully delivers on its "don't load until visible" contract — the previous
per-model index buffer was upfront-loaded and tipped scenes >~1.5 GB into
allocator OOM at frame 1.

Adds residency tracking + a two-phase evictor: (1) drop LRU non-visible
chunks first, (2) if everything resident is visible-this-frame, drop the
farthest-from-eye chunk only when the candidate to load is closer. This
gives monotonic convergence to "closest visible chunks fit the budget"
instead of "first 4 win, rest never load."

Default budget set to 1 GB — explicitly a stopgap, documented inline.
The per-machine OOM ceiling on wgpu-native (caused by allocator
fragmentation from one VkDeviceMemory per createBuffer call) cannot be
solved by tuning this knob. The proper fix is a probed single-pool
buffer with sub-allocation, tracked under task #16.

Caveat: LOD1 indices are now force-disabled when chunking — per-chunk
buffers only carry LOD0. Re-enabling needs LOD1 to participate in the
chunk plan.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:15:11 +10:00
Dion Moult 5a3e5167df wgpu streaming (4/4): per-frame chunk-on-visible loader
The OOM fix for vertex storage. With --streaming, chunks now load on
demand:

  - After cull determines which chunks have visible draws, driveStreamingLoads
    walks non-resident chunks with total_visible_draws > 0 and brings up
    to MAX_STREAMING_LOADS_PER_FRAME (currently 4) into residency.
  - Each load: readSidecarVertexChunk → createBufferWithData →
    buildChunkBindGroup → is_resident = true. Same frame's draw loop
    picks up the newly-built bind_group and renders the chunk.
  - If more non-resident-but-visible chunks remain, requestUpdate is
    called so the load loop keeps running until the visible set is fully
    resident.

Per-chunk bind group construction refactored out of buildModelBindGroup
into a buildChunkBindGroup(m, chunk_idx) helper so the streaming loader
can build one chunk at a time as it arrives.

4 chunks/frame × 60 fps = 240 chunks/sec ingestion. A 200-chunk scene
fully resides in ~1 second of motion. Off-screen chunks never become
resident, never pay vertex-storage VRAM — that's where most of the OOM
fix lands.

Verified on basic.ifc: pixel-identical to non-streaming. On the user's
real 111-model / 1M-instance scene: all metadata loads succeed (was
OOM before), then loader runs but **indices are still loaded upfront
(1.5 GB!) so OOM still hits when vertex chunks start adding on top.**
Per-chunk index deferral is the next commit.

Eager-no-evict policy (per the design conversation): chunks stay
resident once loaded. LRU eviction lands in a follow-up if a workload
proves it necessary.

This completes the 4-commit stage-1 series for task #16. Stage-2:
defer indices, deferred mesh/instance storage if needed, async worker
thread.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 09:23:25 +10:00
Dion Moult f6d888d42b wgpu streaming (3/4): --streaming scaffold + applyCachedModelStreaming
Wires the metadata-only reader (commit 1) through a parallel streaming
load path. With --streaming on:

  - loadSidecar routes through readSidecarMetadataOnly: reads header +
    mesh dict + instance dict + georef + elements upfront. Skips
    vertex bytes entirely.
  - applyCachedModelStreaming computes the same chunk plan as the
    non-streaming path, allocates the small per-chunk buffers
    (visible_draws + prefix_sums + per_chunk_uniform), allocates the
    model-shared mesh + instance + index buffers, but leaves each
    chunk's vertex_storage NULL and is_resident=false.
  - Stores streaming_file_path + vertex_section_offset on the model so
    the per-frame loader can range-read chunks later.
  - Computes per-chunk world AABB by walking instances → mesh → chunk;
    used by both cull (chunk-level frustum reject, future) and the
    streaming loader (proximity-prioritised fetch, future).

Index buffer is still loaded upfront in stage 1 (small relative to
vertex data: ~1/2 of vertex bytes on real scenes). Stage 2 may defer
it too if measurements suggest it's worth the extra plumbing.

Render + pick already gate on c.bind_group (null when non-resident),
so the existing guards correctly skip non-resident chunks without
further changes.

With this commit alone, --streaming mode shows an EMPTY scene (just
background colour) because no chunk ever becomes resident. Commit 4
adds the per-frame loader that triggers chunk load when cull marks
them visible — that's the commit where rendering kicks in and the
OOM fix actually lands.

Default behaviour (no --streaming): legacy synchronous full-load.
Pixel-identical to the prior commit on basic.ifc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 09:15:53 +10:00
Dion Moult d368ee449d wgpu streaming (2/4): per-chunk residency fields on WgpuModelGpuData
Foundation for streaming. Adds to each Chunk:
  - is_resident (default true; streaming flips false initially)
  - vertex_byte_offset / vertex_byte_size in the sidecar file
  - aabb_min / aabb_max world-space chunk bounds (used by future cull
    and streaming priority)

Plus on the model:
  - streaming_file_path (non-empty = streaming path was used)
  - streaming_vertex_section_offset (where the chunks live in the file)

All fields default to backward-compatible values: is_resident=true,
streaming_file_path empty. The existing non-streaming applyCachedModel
sets up a Chunk with is_resident=true (implicit) and ignores the
streaming fields, so no behaviour changes yet.

Commit 3/4 wires the metadata-only reader from (1/4) through a new
applyCachedModelStreaming path that flips is_resident=false initially;
commit 4/4 adds the per-frame loader that brings chunks resident on
demand. This commit is verified pixel-identical to the previous render
on basic.ifc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 09:10:53 +10:00
Dion Moult a06d920fc6 wgpu streaming (1/4): metadata-only sidecar reader
First foundational piece for task #16. WgpuStreamingLoader exposes:

  - readSidecarMetadataOnly(path): reads v13 header + mesh dict + instance
    dict + georef + elements + string table from disk. Skips the bulky
    vertex and index byte sections, recording their on-disk offsets so
    they can be range-read later (per-chunk, on demand). The file handle
    is closed before return.

  - readSidecarVertexChunk / readSidecarIndexChunk: open + fseek + fread
    for a byte range. Synchronous; intended to be called from a worker
    thread for true async streaming or the main thread for stage-1
    on-demand load.

No format change yet — operates on existing v13 sidecars. v14 with an
explicit per-chunk TOC arrives in a follow-up; this layer abstracts
the chunk boundaries so the upgrade stays internal.

No integration with existing applyCachedModel — that's commit 3/4.
Build verifies the API compiles and links into IfcViewerWgpu.

Commits in this series:
  1/4: metadata-only reader (THIS)
  2/4: per-chunk residency state on WgpuModelGpuData
  3/4: --streaming opt-in path through applyCachedModel
  4/4: per-frame chunk-on-visible loader (the OOM fix)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 09:07:33 +10:00
Gorgious56 f1cf757ba2 Refactor bim/module/drawing/gizmos — framework + icon infra
Three concerns bundled into one cohesive refactor of gizmos.py
(splitting them surgically requires intermediate commits with
duplicate same-named classes that Python can't parse):

1. Framework primitives — StaticTrisGizmoMixin + TexturedQuadGizmoMixin
   replace the older TrisGizmoMixin. New module-level helpers:
   _get_static_tris_shader / _get_static_tris_batch / clear_static_
   tris_cache for cached GPU batch reuse, _draw_outline_and_body for
   the shared outline-then-body render path, draw_tris_with_outline
   as the public wrapper. billboarded_at(world_pos, billboard_rot,
   scale) is the canonical billboard-matrix helper; should_flip_extend_
   arrow encapsulates the view-aware mirror decision for extend
   gizmos; get_warning_color_from_prefs reads the user's warning
   color.

2. Config classes — BaseValueGizmoConfig (shared visibility + dimension-
   text contract), CountGizmoConfig (array N indicator),
   DimensionGizmoConfig (length / height / depth labels), IconActionConfig
   (icon-only gizmos that invoke an operator on click). DimensionRenderer
   draws the actual numeric label using BLF.

3. Icon classes — each rewritten on StaticTrisGizmoMixin so they share
   the cached GPU batch + outline-then-body render path:
   GizmoLockOpen / GizmoLockClosed (replacing the single-state
   GizmoLock), GizmoArc, GizmoFillet, GizmoWallCornerIcon,
   GizmoWallTeeIcon, GizmoPen / GizmoValidate / GizmoCancel (the
   parametric-edit triad), GizmoPlus / GizmoMinus / GizmoTrash,
   GizmoArrayParent / GizmoArrayAll / GizmoArrayLayerIndicator (array
   context indicators with a small digit-rendering helper for the "xN"
   count label), GizmoMerge / GizmoSplit / GizmoUnjoin (wall-join
   icons), and GizmoMenu (textured-quad icon-action menu trigger).

The legacy TrisGizmoMixin, GizmoLock, and DimensionDrawConfig are
removed; downstream callers in subsequent PR4 commits swap to the
new mixin and config classes when their feature operators land.

CycleTypeMixin / PickTypeMixin / TypeAccessorBase live in
bim.parametric_lifecycle (previous commit). The three mixins are
re-exported from gizmos.py here so feature-module access via
``gizmo.<MixinName>`` keeps working until PR5 cleanup drops the
re-exports.

bim/module/drawing/__init__.py is updated in the same commit to
register the 11 new gizmo classes (GizmoLockOpen / GizmoLockClosed /
GizmoFillet / GizmoWallCornerIcon / GizmoWallTeeIcon / GizmoTrash /
GizmoArrayParent / GizmoArrayAll / GizmoArrayLayerIndicator /
GizmoUnjoin / GizmoMenu) — without that, the new classes exist in
gizmos.py but aren't usable as bpy gizmo types.

Generated with the assistance of an AI coding tool.
2026-05-27 23:56:28 +02:00
Gorgious56 b039e12623 Add TypeAccessorBase + CycleTypeMixin + PickTypeMixin
Three operator mixins for type-selection ops on parametric features
(door type-cycle, window type-pick, stair type-cycle, railing
type-pick, roof type-cycle, etc.). Each shares the same contract:

* ``element_checker`` validates the active object is the expected
  IFC type
* ``props_getter`` resolves the BIM<Name>Properties group
* ``type_literal`` is the Literal type whose args drive the enum
* ``type_attr`` is the PropertyGroup field to read/write
* ``skip_element_check=True`` bypasses element validation (for
  operators that target a non-IFC context)

CycleTypeMixin shift-click reverses direction (forward by default).
PickTypeMixin opens a popup menu and routes the picked value
through execute() so F6 redo / EXEC_DEFAULT reach the apply path.
The PickType modal-handler dance waits for LEFTMOUSE release before
opening the menu when invoked mid-click (e.g. from a gizmo's
target_set_operator) so Blender's drag-through-pick gesture doesn't
commit an accidental item.

Ships standalone — the next commit's gizmos.py framework refactor
re-exports these names from bonsai.bim.parametric_lifecycle so
gizmo modules can spell ``gizmo.CycleTypeMixin`` / ``gizmo.PickTypeMixin``.
Concrete operator subclasses land in subsequent PR4 commits per
feature (door / window / stair / railing / roof).

Generated with the assistance of an AI coding tool.
2026-05-27 23:06:42 +02:00
Gorgious56 1325705d8e Merge pull request #8112 from Gorgious56/bonsai/parametric-framework-infra
Decorator cache + parametric lifecycle drift triad + wall split fixes
2026-05-27 21:43:51 +02:00
Gorgious56 cb2f20b2b6 Add tests for decorator_cache + undo-resync dispatch
Two paired test files for the framework infrastructure landed
earlier in this PR.

test_decorator_cache.py (11 tests):
* The 4-hook invalidation list (depsgraph_update_post + undo_post +
  redo_post + load_post) is symmetrically managed by
  install_decorator_cache_handlers / uninstall_decorator_cache_handlers.
  A future edit that drops a hook from one side without the other
  would land as a Blender segfault when a cached bpy.types.Object
  ref outlives its underlying ID block — the regression must surface
  as a test failure first.
* install is idempotent (calling twice doesn't double-register).
* uninstall when not installed doesn't raise.
* The bump handler accepts Blender's variadic args.
* The depsgraph predicate gates correctly: bumps on Object geometry
  or transform updates, silently skips on Material / NodeTree / Image
  updates (which would otherwise rebuild every cache on every node
  edit).
* TokenCache.get_or_compute short-circuits on key+token match and
  recomputes when the token bumps.

test_undo_resync_parametric_drafts.py (3 tests):
* UNDO_REGENERATORS keys must all be in tool.Parametric.EDIT_TYPES.
  A typo would silently no-op on Ctrl+Z, restoring the desync the
  helper is meant to prevent.
* The dispatcher skips objects with no active parametric edit
  (undo_post fires for every undo, most of which touch zero drafts).
* The dispatcher silently skips parametric types that have no
  UNDO_REGENERATORS entry (door / window / array are IFC-derived
  with no draft preview mesh — they don't need a regenerator).

Mocks use spec=bpy.types.Depsgraph / spec=bpy.types.DepsgraphUpdate
/ spec=tool.parametric.ParametricObject so typos in mocked-attribute
access fail loudly (CLAUDE.md test discipline).

Generated with the assistance of an AI coding tool.
2026-05-27 21:28:41 +02:00
Gorgious56 f41f5dfdd8 Fix wall split: preserve door/window fill rel
Splitting a wall through a door orphaned the door (door.FillsVoids
became empty). The fill rel was being reassigned by setting its
RelatedBuildingElement slot — schema-wise that's the filling slot, not
the wall slot — so when remove_feature deleted the old opening it
also cascade-removed the rel. Transferring via RelatingOpeningElement
keeps the rel pointing at the new opening so the door stays
associated. Pre-existing bug from 5a6476a57, surfaced by ef144dce2.

Generated with the assistance of an AI coding tool.
2026-05-27 21:28:41 +02:00
Gorgious56 1855e4c019 Fix wall split: keep straddling openings on both walls
DumbWallJoiner.split assigned openings by projecting the opening's
centre-point onto the wall axis, so any opening whose footprint
straddled the cut was silently dropped from whichever wall its centre
missed. Now the full axis-projected extent (via ifcopenshell.geom.
create_shape) drives the assignment; for filled openings whose void
straddles the cut, a pure-void copy is added back to the neighbour
wall so its body is also cut.

Generated with the assistance of an AI coding tool.
2026-05-27 21:28:41 +02:00
Gorgious56 2feade01cb DRY tag-redraw-3D-viewports loops via tool.Blender.update_all_viewports
Five inline copies of the same defensive pattern lived across
``tool/parametric.py``, ``bim/parametric_lifecycle.py``,
``bim/module/model/preview_base.py`` (twice), and as a near-twin
in ``tool/blender.py:update_all_viewports`` itself.

``tool.Blender.update_all_viewports`` already covered the
``tag_redraw`` job but used an ``assert context.screen`` that would
raise during background-mode operators or early-load_post calls
where ``screen`` legitimately is None. Relax to a defensive
``getattr(context, "screen", None)`` + silent return so the helper
fits every caller's needs, then collapse the 4 inline copies to
single calls.

Net -9 LOC. The helper now describes its contract ("silent no-op
when no screen attached") rather than naming specific callers, so
moving a caller doesn't rot the docstring.

Generated with the assistance of an AI coding tool.
2026-05-27 21:28:41 +02:00
Gorgious56 ff4c642db1 Add parametric-draft undo-resync registry
Ctrl+Z / Ctrl+Shift+Z on an in-progress parametric draft (wall /
stair / roof) used to leave the preview mesh frozen in its
pre-undo shape — the IFC mutation rolls back but the bmesh built
from draft props doesn't repaint.

Add a registry of per-type regenerator functions
(``UNDO_REGENERATORS``) that re-build each type's preview mesh
from its current props. The dispatcher
``resync_parametric_drafts_after_undo`` walks all objects, skips
any without an active parametric edit, looks up the regenerator
by feature name, and calls it. Tagged 3D viewports for redraw.

Types without an entry (door / window / railing / etc.) are
intentionally absent — they're IFC-derived, so the undo's
representation rollback + next-frame refresh already repaints
correctly without a draft-side regenerator.

Undo/redo wiring is self-installed by
``bonsai.bim.parametric_lifecycle``: a ``@persistent``
``_resync_on_undo`` callback dispatches into the registry, and
``install_parametric_lifecycle_handlers()`` /
``uninstall_parametric_lifecycle_handlers()`` append/remove it
from ``bpy.app.handlers.undo_post`` and ``redo_post``.
``bim/__init__.py``'s ``register()`` calls the install function
*after* the central ``handler.undo_post`` / ``redo_post`` appends
so the regenerators see restored IFC state — ``bpy.app.handlers``
fire in append order. ``handler.py`` itself stays ignorant of the
parametric subsystem. The lazy function-local imports in each
regenerator break the addon-load cycle —
``bonsai.bim.parametric_lifecycle`` loads before
``bim/module/model/*``.

Generated with the assistance of an AI coding tool.
2026-05-27 21:28:20 +02:00
Gorgious56 e7e489e390 Refactor bim/parametric_lifecycle — drift triad + Cancel polish
Three changes to the shared Enable/Finish/Cancel mixins:

1. Always-on drift triad on ParametricEditMixinBase. The base now
   provides ``_handle_drift_on_enable`` / ``_handle_drift_on_finish``
   / ``_handle_drift_on_cancel`` classmethods, called from the
   per-mixin ``_enable_one`` / ``_finish_one`` / ``_cancel_one``.
   Pre-edit Blender-side translations commit to IFC on Enable
   (apply_scale=False — only translation/rotation, not the user's
   accidental scale), in-edit drag commits on Finish (apply_scale=True),
   and Cancel restores the committed IFC placement via
   ``restore_or_rebaseline_placement``. Prevents the
   "uncommitted drag disappears on Finish" and "preview snaps back
   on Cancel" UX bugs.

2. ``_ParametricEditMixinBase`` renamed to ``ParametricEditMixinBase``
   (public). Per-feature mixins that need to subclass directly
   (e.g., when neither FeatureModifier nor PathPreserving fits)
   can do so without reaching into a private name.

3. ``_update_modifier_bmesh`` (PathPreserving) renamed to
   ``_restore_viewport_after_cancel``. The old name was inaccurate
   for subclasses that load a different IFC representation on
   Cancel rather than rebuilding a bmesh preview from props.

Plus two polish changes:

* ``_mark_type_thumbnail_dirty`` helper on the base centralises the
  ``ifcopenshell.util.element.get_type`` + thumbnail-mark pattern
  that both mixins repeated inline.
* ``FeatureModifierEditMixin._cancel_one`` and
  ``PathPreservingEditMixin._cancel_one`` wrap the restore in
  ``try/finally`` so ``props.is_editing = False`` flips even on
  partial restore failure. Without this, a Cancel that raised
  mid-restore would leave the user locked out of the edit lifecycle.
* ``PathPreservingEditMixin._finish_one`` / ``_cancel_one`` skip the
  pset commit + viewport rebuild when the draft equals the stored
  pset (no-op Enable→Finish round-trip should not pollute the
  representation list or burn an undo entry).

``FeatureModifierEditMixin._finish_one`` now routes the pset commit
through ``tool.Pset.write_bbim_data`` instead of inlining the
``createIfcText(json.dumps(...))`` + ``ifcopenshell.api.pset.edit_pset``
dance. Two test assertions updated to match.

Generated with the assistance of an AI coding tool.
2026-05-27 15:51:37 +02:00
Gorgious56 5e23030a0f Decompose bim/handler.py load_post + install cache + discard hooks
Three concerns folded into ``load_post`` argue for separation:

1. Save-file invariants every load must re-establish (msgbus
   subscription, owner-settings, thumbnail cache, draft-flag healing,
   blend-warning flag, H5 lock probe).
2. User-preference-driven UI setup (toolbar, workspace, viewport
   shading, panel hijack, snap defaults).
3. Viewport overlay sync (every decorator's install/uninstall).

Pull each into its own function (``_apply_save_file_invariants`` /
``_apply_user_preferences`` / ``_install_viewport_overlays``). The
``load_post`` callback becomes a 3-line orchestrator. Each phase
is independently call-able from tests and from PR4 features that
need to re-trigger one phase without the others.

Two new hooks land with the decompose:

* ``tool.Parametric.heal_stale_edit_flags()`` + ``discard_pending_previews(scene)``
  fire in ``_apply_save_file_invariants``. The first clears
  object-level ``BIM<Name>Properties.is_editing`` flags that lost
  their backing IFC element across a load; the second clears
  scene-level ``BIMPreviewProperties.<x>.is_active`` so saved
  preview state never resurfaces with no UI to interact with it.

* ``install_decorator_cache_handlers`` / ``uninstall_decorator_cache_handlers``
  wrap the decorator install/install pass in
  ``_install_viewport_overlays``. The bump handlers append to
  ``depsgraph_update_post`` + ``undo_post`` + ``redo_post`` +
  ``load_post`` so the previous commit's ``TokenCache`` in
  ``tool.System.get_decoration_data`` finally invalidates on
  structural scene changes.

Generated with the assistance of an AI coding tool.
2026-05-27 15:28:18 +02:00
Gorgious56 c9f12dd441 Add bim/module/model/preview_base module
Shared helpers for Bonsai's Scene-level parametric preview flows.
Two PR4 features will consume this — MEP bend preview and wall
fillet preview — both following the same shape:

    Enable<X>Preview   — populates draft on Scene.BIMPreviewProperties.<x>
    Gizmo<X>Preview    — polls on is_active, surfaces tunable widgets
    <X>PreviewDecorator — GPU lines while is_active is True
    Finish<X>Preview   — bpy.ops.bim.<verb>(...) with draft kwargs
    Cancel<X>Preview   — pure state reset

The module hosts the cross-cutting accessors (``get_preview_props``,
``is_preview_active``), lazy-closure factories for gizmo dimension
callbacks (``make_props_callback`` / ``make_dim_getter`` /
``make_dim_setter`` — defensive against missing scene / freed RNA
struct on file open / undo), the Enable-time IFC-placement sync
(``sync_uncommitted_moves``), and the Esc + load_post discard
machinery (``PREVIEW_CANCEL_OPS`` registry, ``try_cancel_active_preview``,
``discard_pending_previews``).

Ships standalone — the consumer features land in PR4 (preview
PropertyGroups, Enable/Finish/Cancel operators, gizmo groups,
decorators, Esc keymap binding). All accessors are defensive
against missing PropertyGroups / operators on v0.8.0 — calling
``discard_pending_previews(scene)`` from the next commit's
load_post hook is a no-op until PR4 attaches BIMPreviewProperties.

Generated with the assistance of an AI coding tool.
2026-05-27 15:25:07 +02:00
Gorgious56 4b9ad66c95 Wrap tool.System.get_decoration_data with TokenCache lookup
System decoration draws on every viewport refresh — the
``_build_decoration_data`` body walks every distribution element,
resolves connected ports, builds the vert/edge arrays for the GPU
batch. A bare call per frame burns time on an unchanged scene.

Add a single-entry cache keyed on ``(decorator_cache_token,
id(decorated_elements_set))``. Reads short-circuit when neither
component moved:

* ``decorator_cache_token`` from ``bim.decorator_cache`` invalidates
  on depsgraph / undo / redo / load via the bump handler.
* ``id(decorated_elements_set)`` invalidates when
  ``SystemDecorationData.load()`` reassigns the set (e.g. when the
  user changes the set of decorated systems via the panel).

The handler that bumps the token is installed in the next commit
(bim/handler.py decompose). Until then the token stays at 0, so
the cache only hits when ``id()`` also matches — degraded behaviour
during the bisect window but not incorrect.

Generated with the assistance of an AI coding tool.
2026-05-27 14:55:44 +02:00
Gorgious56 d43a1353e0 Add bim/decorator_cache module — TokenCache + handler primitives
New helper module for POST_VIEW decorators. Exports:

* ``get_decorator_cache_token()`` — global int counter consumers
  include in their cache key so the value invalidates on structural
  scene changes.
* ``_bump_decorator_cache_token()`` — ``@bpy.app.handlers.persistent``
  callback that increments the token. Gates on the depsgraph payload
  so animation playback / driver evaluation doesn't churn the token.
* ``install_decorator_cache_handlers`` / ``uninstall_…`` — idempotent
  append / remove against depsgraph_update_post + undo_post + redo_post
  + load_post. Called once from ``bim.register`` / ``unregister``.
* ``TokenCache[T]`` — single-entry memoiser keyed on ``(caller_key,
  token)``. Cached ``bpy.types.Object`` references can't outlive the
  underlying ID blocks because any depsgraph / undo / load bumps the
  token and forces a recompute.

This commit ships the module standalone. The next commits in this
PR wire it: tool/system.py adds the cache wrap on get_decoration_data
and bim/handler.py installs the bump callbacks. Until both land,
the module is intentionally dead code — keeps the diff narrow and
the commit history bisectable.

Generated with the assistance of an AI coding tool.
2026-05-27 14:53:06 +02:00
Gorgious56 b1fa2407a9 Merge pull request #8109 from Gorgious56/bonsai/parametric-framework-slim
Extract parametric framework foundation into tool/ and core/
2026-05-27 14:46:59 +02:00
Gorgious56 786d3c8a89 Fix latent runtime bugs + ty annotations surfaced by CI
Five code paths in slim PR2 referenced symbols that don't exist in
v0.8.0's bim layer, raising at first call. Plus three type
annotations that ty flagged as unresolved.

1. tool/system.py:get_decoration_data — drop the cache layer that
   keyed on a token from a bim/decorator_cache.py module. The cache
   is dead-or-broken in slim: the depsgraph bump handler that would
   invalidate the token lives in PR3's bim/handler.py decompose, so
   the token stays at 0 forever. Either the cache never hits
   (decorated_elements rebuilt → new id() per call) or returns
   stale data (list reused). Revert to direct
   `_build_decoration_data()` calls. PR3 reintroduces the cache
   atomically: decorator_cache module + handler install + cache
   wrap + tests. Keeps `_build_decoration_data` extraction
   (cleaner than v0.8.0's monolithic version regardless of cache).

2. tool/spatial.py — add `get_host_element` + `get_host_wall`.
   The interface stubs in `core/tool.py:1037-1038` were declared
   but never implemented. `tool/duplicate.py:99` (object duplication
   with fills) and `tool/model.py:1260` (array per-child opening
   mirror) call these and would raise AttributeError.

3. tool/model.py:recreate_wall — drop the fillet-corner branch
   that function-locally imports `regenerate_fillet_corner_wall`
   from `bim/module/model/wall`. The function lands with PR4; fall
   through to the straight-extrusion path preserves v0.8.0
   behaviour for fillet walls until then. Tag FIXME(PR4).

4. tool/model.py — drop `get_pipe_segment_props` /
   `get_duct_segment_props` accessors. Their return types reference
   `BIMPipeSegmentProperties` / `BIMDuctSegmentProperties` which
   land with PR4's prop.py; calling either accessor on v0.8.0 would
   AttributeError on `obj.BIM<X>SegmentProperties`. Zero callers in
   slim — PR4 reintroduces both accessors together with the
   PropertyGroups they wrap. Also drops the matching TYPE_CHECKING
   imports.

5. tool/blender.py:557 — `Mapping[type[ViewportDecorator], bool]`
   needs the qualified `Blender.ViewportDecorator` because the
   annotation is on a method INSIDE the same nested class; the
   bare name doesn't resolve at type-check time.

6. core/tool.py Surveyor — drop the `obj: "bpy.types.Object"` /
   `z: float` / `-> float` / `-> None` annotations on
   `get_z_rotation` / `set_z_rotation`. The `@interface` decorator
   wraps each method as `classmethod(abstractmethod(...))` at
   import time, but ty doesn't track the wrap and flags every
   call site as `missing-argument` plus the `pass` body as
   `empty-body` against the declared return type, plus the
   `bpy.types.Object` forward-ref as `unresolved-reference`.
   Reverting to v0.8.0's untyped style (matching the sibling
   `get_absolute_matrix(cls, obj)` stub) clears six ty errors at
   the cost of zero runtime semantics — the abstract stubs only
   serve as registry markers, concrete `tool.Surveyor.*` carries
   the real signatures.

Generated with the assistance of an AI coding tool.
2026-05-27 14:38:37 +02:00
Dion Moult a1693259b8 wgpu backend: BVH cull (opt-in via --bvh, default off)
Stage 15 implementation lands but doesn't pay off as default-on. On a
562k-instance / 18-model scene with a centred camera, the BVH walk
adds ~10 ms of cull cost without rejecting enough subtrees to
compensate — every interior node's AABB straddles the frustum, so
descents go all the way to leaves anyway. Linear scan beats it by
that 10 ms.

GL's BVH works better mainly because they do full cull (frustum + HiZ
+ contribution) at every node — their per-test cost is lower (likely
SIMD-vectorised) and they get more subtree rejections. My current
impl does frustum-only at interior nodes (HiZ there cost more than
it saved on the smaller dataset).

For now, gate the whole BVH walk behind --bvh, default off. The
infrastructure (BvhAccel build at applyCachedModel, walk in cull,
release) stays in place so it's a one-flag toggle to measure either
side. Real default-on requires further tuning — see updated task #15.

Measured on 562k-instance scene:
  --bvh on  → 25.9ms total (cull 25.4ms)
  --bvh off → 15.4ms total (cull 14.5ms)   ← default

For comparison, GL on the same scene + camera:
  GL → 18.2ms total (cull 8.5ms wall, multi-threaded BVH)

Net: wgpu beats GL by ~3ms total despite slower cull, because the
GPU side (no edge-pass cost, async HiZ readback, lean main pipeline)
gives back more than the cull deficit.

Also added task #17 (GPU compute-shader cull) as the asymptotic
answer — both backends hit CPU cull as the ceiling on ≥500k scenes;
moving it to a compute shader drops it to sub-ms regardless.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:04:44 +10:00
Gorgious56 89b7eff03e Add addon-load smoke test pinning register/unregister cycle
Surfaces any regression in:

* the modules dict in bim/__init__.py (added a folder, forgot the entry)
* PointerProperty wiring on bpy.types.{Scene,Object,...}
* registry-driven GizmoPreferences<Name> auto-registration in
  tool.Parametric.iter_gizmo_preference_classes
* bpy.app.handlers append/remove balance
* every register()/unregister() across the 45+ feature modules

as a single PASSED/FAILED test instead of the silent "addon failed to
enable" users encounter in a fresh Blender. Paired with the existing
test_parametric_registry.py contract tests, this catches both the
registry-shape regressions (operators/PropertyGroups/predicates) and
the registration-mechanics regressions (PointerProperty types not
registered before their owners).

Generated with the assistance of an AI coding tool.
2026-05-27 13:26:38 +02:00
Gorgious56 1c8fad3c13 Fix tool.Parametric to ship safely on v0.8.0 bim layer
Three corrective fixes folded into one commit. All surface as
addon-load / save-time exceptions on v0.8.0's bim layer because
PR2's tool.Parametric refactor over-committed to the PR4 contract.

1. iter_gizmo_preference_classes — the previous implementation
   returned only the shared GizmoPreferencesFeature class. v0.8.0's
   bim/ui.py declares PointerProperty fields ('door', 'window', ...)
   on GizmoPreferences that point at per-feature
   GizmoPreferences<Name> classes; those must be registered BEFORE
   GizmoPreferences itself. The shared-class-only return broke
   addon registration with:
      'door' PointerProperty could not register (see previous error)
   Restore the v0.8.0 per-feature lookup (iterate EDIT_TYPES, look
   up each GizmoPreferences<Capitalize(name)> on ui_module) and
   keep the shared-class lookup as forward-compat. Tag FIXME(PR5).

2. EDIT_TYPES — drop the array / pipe_segment / duct_segment
   entries from the registry. Their bim.finish_editing_<name>
   operators land with PR4. Registering them in PR2's EDIT_TYPES
   without the operators makes auto-commit-on-save dispatch a
   non-existent finish_op for any object whose
   BIM<Name>Properties.is_editing flag is True, raising:
      RuntimeError: 'bim.finish_editing_array' must be a registered
      tool.Ifc.Operator subclass for undo-safe IFC mutation
   PR4 re-adds the three entries together with their operators.
   Tag FIXME(PR4).

3. tool.Blender.Modifier shim block — upgrade the prose comment to
   a formal FIXME(PR5) marker so the PR5 cleanup sweep finds it via
   grep alongside every other tagged shim site.

Generated with the assistance of an AI coding tool.
2026-05-27 13:26:21 +02:00
Dion Moult 7dc13eb104 wgpu backend: chunk vertex storage to fit browser limits + settle frame
Two pieces:

1. Per-chunk vertex storage (stage 13)
   WebGPU mandates maxStorageBufferBindingSize ≥ 128 MB. Real BIM models
   routinely exceed that (one of yours is 139 MB vertex). Without
   chunking, every browser load would fail with
   "exceeds max_storage_buffer_binding_size".

   Strategy: each model's vertex data is split into ≤ 128 MB chunks at
   applyCachedModel time. Each chunk gets its own vertex_storage buffer,
   visible_draws / prefix_sums buffers, per_chunk_uniform, and bind group.
   Index buffer, instance storage, and mesh storage stay single-per-model
   (they fit well under the cap on every scene we've seen). Mesh-to-chunk
   assignment is bake-time-deterministic (walks meshes in order, opens a
   new chunk when adding the next would overflow).

   Cull buckets visible instances by their mesh's chunk; render issues
   one drawcall per non-empty chunk per model. WGSL is unchanged — the
   binary-search vertex pulling works identically per chunk because
   base_vertex is now CHUNK-LOCAL (the chunk's bind group binds its own
   vertex_storage).

   Single code path: chunking is ALWAYS on at 128 MB regardless of
   target. Cost on desktop is a handful of extra drawcalls per frame
   (1 per non-empty chunk; typical models = 1-3 chunks). Negligible.

   A mesh whose vertex range is itself > 128 MB can't fit in any chunk
   and would need splitting — typical IFC meshes are nowhere near that
   (hundreds of verts), and applyCachedModel warns loudly if one ever
   appears.

   --web-limits CLI flag requests the WebGPU mandatory floor limits
   (128 MB max storage binding, 256 MB max buffer) instead of the
   adapter's actual max. Used to verify chunking actually fits through
   browser constraints — turns "trust me, web will work" into a hard
   test. The 139 MB scene loads cleanly with --web-limits.

2. Settle frame after motion (bug fix)
   Reported regression: after orbiting, sub-pixel instances dropped by
   motion-mode contribution culling stayed missing after the camera
   stopped. Event-driven rendering means no frame is scheduled after
   mouse-up, so the cull never re-ran at the still threshold.

   Fix: track last_cull_was_motion_. If this frame used the motion
   threshold, requestUpdate() after present to schedule one settle
   frame. Next frame: camera_moved = false → still threshold → small
   instances reappear. Matches GL's last_cull_was_motion_ behaviour.

Verified pixel-identical on basic.ifc; loads the user's dense scene
successfully under --web-limits (chunks=2 on the 139 MB model,
chunks=1 on the others).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 21:05:38 +10:00
Dion Moult 5810894eb8 ifcviewer (GL minimal): --screenshot for parity diff with wgpu
Closes the other half of task #10. The wgpu minimal already wrote PNGs
via wgpuCommandEncoderCopyTextureToBuffer + mapAsync; the GL backend
now has the equivalent via glReadPixels on the back buffer just before
swapBuffers.

  - ViewportWindow::captureNextFrameToPng(path, quit_after=true) queues
    a one-shot capture. render() reads the default framebuffer at full
    pixel size (width * devicePixelRatio), flips bottom-up → top-down
    into a QImage::Format_RGBA8888, saves PNG, and optionally
    QCoreApplication::quit. Synchronous glReadPixels is fine here —
    pick is interactive and rare; not used per-frame.

  - ifcviewer-minimal --screenshot PATH wires through MinimalWindow
    just like --camera / --benchmark. Honoured after all loads complete
    (applyPendingBenchmark also drains pending_screenshot_).

Lets a parity script do:
    IfcViewerMinimal      foo.ifc      --camera A,B,C,D,E,F --screenshot gl.png
    IfcViewerWgpuMinimal  foo.ifcview  --camera A,B,C,D,E,F --screenshot wgpu.png
    # then pixel-diff with whatever (ImageMagick, PIL, etc.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:33:20 +10:00
Dion Moult 4dfe27e251 wgpu backend: per-element visibility + H/Shift+H/I hotkeys
Closes the interactive selection loop. After this commit you can:
  - LMB-click an object  → highlight (selection)
  - Press H              → hide all selected
  - Press Shift+H        → show all (clear hidden set)
  - Press I              → isolate selected (hide everything else)

WgpuVisibilityState (new header) is a plain unordered_set<uint32_t> of
hidden object_ids — mirrors src/ifcviewer/Visibility.h's shape but
stays Qt-free for the ifcviewer-core extract later.

cullModelCpuCompute consults visibility_.isHidden(inst.object_id)
before the frustum test — hidden instances cost nothing on every axis
(no draw, no depth contribution, no pick hit). The CPU vector is
read concurrently by the parallel cull workers, which is safe because
mutations only happen between renders (handlers requestUpdate after
mutating; render reads).

Hiding deselects (matches GL behaviour: H clears the now-invisible
selection rather than leaving phantom selected-but-invisible ids).

Stage 5's last piece — clip planes — is deferred. Adding the uniform
array + WGSL discard is mechanical, but the section-tool UI that
drives them isn't ported yet (minimal viewer has no way to place a
clip plane), so it'd ship as empty plumbing. Will land alongside the
section-tool port.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:28:07 +10:00
Dion Moult 54fa7d8379 wgpu backend: selection visualisation + global object_id rebase
Closes the loop on stage 4 (pick): clicking an object now highlights it
on screen. Plus the prerequisite plumbing for selection to behave
correctly across multi-sidecar loads.

Pieces:

  1. WgpuSelectionState (new header)
     CPU-side multi-set + active-id, mirroring the GL Selection.h shape
     but pure stdlib (no Qt deps) so it can move into ifcviewer-core
     later without dragging Qt across. clear/replace/add/remove/toggle
     APIs + a fillFlagsArray helper that packs (selected, active) into
     a u32 bitmap indexed by object_id.

  2. selection_flags storage buffer + frame_bgl bump to 2 entries
     Indexed by object_id, bit 0 = selected, bit 1 = active. Lives in
     the frame bind group (group=0 binding=1) because object_ids are
     globally unique — making it model-scoped would be the wrong cut.
     ensureSelectionFlagsBuffer grows geometrically (64 → 128 → … u32)
     as new models push next_object_id_ up, rebuilds the frame bind
     group when it does.

  3. Global object_id rebase in applyCachedModel
     Each sidecar's local ids start from 1 and collide across files;
     pick was previously ambiguous on multi-model loads. We now add
     next_object_id_ as a base offset, rewrite InstanceCpu.object_id
     (CPU mirror stays consistent) + InstanceGpu.object_id (what pick
     reads back), and bump next_object_id_ by the model's max + 1.

  4. WGSL main fragment reads sel_flags
     Vertex shader passes inst.object_id through to fragment as
     @interpolate(flat). Fragment reads sel_flags[object_id], mixes
     (0.2, 0.6, 1.0) at 0.45 for in-selection and (0.4, 0.8, 1.0) at
     0.40 on top for active. Same constants as the GL main shader.

  5. Mouse → selection
     LMB-click-without-drag pick result feeds the selection:
       no modifier → replace
       Shift      → add
       Ctrl       → remove (active migrates to another id in the set)
       miss + no modifier → clear
     uploadSelectionFlagsIfDirty repacks + writes the GPU bitmap at
     the top of the next render(); no upload on still frames.

Pick pipeline is unchanged — it already outputs the per-instance
object_id, and that's what the selection storage indexes.

Visibility + clip planes are pending follow-ups in stage 5 (mostly
small, share the same buffer-lifecycle pattern). Edge silhouette
(stage 9 partial), --screenshot diff harness (stage 10 partial),
ifcviewer-core extract (stage 12), and web chunking (stage 13) all
still pending.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:25:05 +10:00
Dion Moult 1bbcd10bc0 wgpu backend: pick pass with sync R32UInt readback
Stage 4 of the wgpu port. LMB-click-without-drag now resolves the
object_id under the cursor by running a dedicated pick render and
copying back the single texel at the click position.

  - Pick pipeline reuses the existing pipeline_layout_ (same bindings
    as main: frame uniform at group=0, per-model storages at group=1).
    Different vs / fs entry points (vs_pick / fs_pick) in the main
    WGSL module — the vertex pulling logic is duplicated for now but
    the bind group layout match means no pipeline_layout rebuild and
    pickObjectAt can reuse the current frame's already-uploaded
    visible_draws + per-model bind groups.

  - Pick FBO: surface-sized R32UInt color attachment + Depth32Float
    depth, both single-sample (no MSAA — pick needs exact texel
    access). CopySrc on the color so we can copyTextureToBuffer the
    1×1 click region. Recreated on surface resize.

  - pickObjectAt: encodes a one-shot pick pass + a single texel copy
    into a 256-byte staging buffer, submits, mapAsync, sync-spins
    processEvents until ready. Synchronous wait is fine here — pick
    runs on click, not per-frame, so a sub-ms stall is invisible.

  - Mouse integration: existing LMB drag-orbit preserved. A 3-pixel
    threshold promotes drag (set nav_dragged_); release without
    dragging triggers pickObjectAt at the release coords (logical
    Qt → physical pixels via devicePixelRatio). object_id is logged;
    selection state to consume the id arrives with stage 5.

Object_id 0 means miss (clear value); the pick attachment is cleared
to 0 before each pass and the fragment writes the instance's
object_id, so any non-zero result is a real hit on a drawn instance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:12:38 +10:00
Dion Moult f4243038bd wgpu backend: edge silhouette post-process (ported GL renderEdgePass)
First piece of stage 9 — the dark outlines BonsaiViewer / the GL backend
draw at depth discontinuities. Ported the GL renderEdgePass algorithm
verbatim, including the three things my earlier attempt missed:

  1. Linearise depth to view-space metres before the Laplacian. Raw
     [0,1] clip-z is heavily non-linear so a fixed-threshold edge
     detector only caught near-camera silhouettes. Now reverses the
     wgpu z-remap (z * 2 - 1 back to GL NDC) then standard reverse-
     perspective to view-z.

  2. Threshold scales with depth: t = EDGE_THRESHOLD * c. A 4 mm gap
     between two surfaces reads the same whether it's 0.5 m or 50 m
     away from the camera.

  3. Multiplicative blend (Dst, Zero) with fragment output of
     vec3(1 - edge). Strictly darkens, never brightens. Matches GL's
     (GL_DST_COLOR, GL_ZERO) blend.

Constants EDGE_SCALE=6.0 / EDGE_THRESHOLD=0.004 are GL's tuned values.
Camera near/far hard-coded to 0.1 / 10000 (the viewport defaults);
they'll move to a small uniform when AppSettings ports across.

Pipeline state: depth-attachment-less, sample count 1, blend on, no
cull. Reuses depth_texture_'s TextureBinding usage that HiZ added.
Render pass loads the resolved main-pass colour (LoadOp_Load) and
writes back through the multiplicative blend; encoded between the main
pass and the HiZ resolve so HiZ uses the same MSAA depth that produced
the edges. edge_bind_group_ rebuilds lazily when depth_view_ is
replaced (mirrors the HiZ bind group lifecycle).

Perf cost on the 10-sidecar / 380k-instance benchmark: 0.1 ms (11.5 →
11.6 ms). Fullscreen depth-laplacian is essentially free on this GPU.

Remaining stage 9 work: HUD/labels/lines/points overlay primitives,
which need the QPainter-→-texture path. Lower visual priority than
edges; handled in a follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:54:29 +10:00
Dion Moult 51dc31a50b wgpu backend: 10× perf — megadraw, async HiZ, parallel cull, motion mode
Closes the perf gap to the GL backend on real BIM benchmarks. On a 10-
sidecar / 380k-instance corpus at a fixed --camera the wgpu binary went
from 110.6 ms to 11.6 ms (vs GL's 23 ms — half the frame time, but
note GL is doing extra work the wgpu backend hasn't ported yet; see
the caveats list at the bottom). Bundled because the pieces interlock
and shipping any of them without the others reintroduces the same wall.

1. Cross-mesh vertex pulling (single mega-draw per model)
   The previous one-drawIndexed-per-(mesh × LOD-bucket) loop was costing
   ~13ms on a 27k-mesh scene. CPU now emits a flat visible_draws[]
   (16 B per visible (mesh,lod,instance)) plus a prefix_sums[] table.
   WGSL binary-searches prefix_sums by @builtin(vertex_index) to find
   the entry, then manually fetches the mesh-local index from a
   storage-bound indices[] and pulls the packed 12 B vertex. No
   setIndexBuffer; the shader reads everything from storage. Bind
   group grew from 4 to 7 entries (vertices, meshes, instances,
   indices, visible_draws, prefix_sums, per-model uniform) — well
   under WebGPU's mandatory 8 storage / 12 uniform floor.

2. Async HiZ readback via ping-pong staging buffers
   Sync wait via wgpuInstanceProcessEvents was costing ~37 ms on a
   real scene (GPU drain). Two staging slots now ping-pong: frame N
   kicks a non-blocking mapAsync on slot K, frame N+1's first action
   is one processEvents drain. Pyramid is 1-2 frames stale — matches
   the "slightly-stale depth, fine" pattern the GL backend already
   documents. encodeHizResolve returns -1 (skip) if both slots are
   in flight; cull keeps using the most recent pyramid.

3. Cull reorder: contribution before HiZ
   HiZ projection is ~10× more expensive than the contribution
   check, yet most contribution-survivors would be HiZ-rejected
   anyway on dense scenes. Computing projected_px first lets
   contribution short-circuit ~80% of HiZ tests with no rejection-
   quality loss. Saved ~34 ms on the dense bench.

4. Motion-mode contribution threshold
   AppSettings::motionMinPixelRadius parity. While the camera is
   changing (orbit/pan/zoom/--benchmark sweep), drop instances
   below 10 px instead of 2 px. Halves visible_objects during
   motion with no perceived quality loss.

5. Parallel cull (std::async across models)
   Per-model cullModelCpu split into Compute (CPU-only, thread-safe)
   + Upload (main-thread wgpu queue writes). std::async fan-outs the
   compute across models; main-thread joins and uploads. Wall-clock
   cull on the 10-model corpus drops from ~17 ms single-threaded to
   ~9 ms across cores.

6. --no-hiz CLI flag + per-phase benchmark timings
   Benchmark now also prints "per-frame avg ms: cull=X
   hiz_readback=Y" so future regressions can be attributed without
   guesswork. --no-hiz toggles the master switch from the CLI.

Honest caveats — wgpu is currently faster mostly because GL is doing
work we haven't ported yet:
  - Edge silhouette pass (stage 9) will add ~3-5 ms back to wgpu.
  - GL's HiZ uses the BVH so it rejects whole subtrees (1.7k vs
    our 358 rejects on the same scene). BVH for HiZ is future work
    (task #13 / a new task) — until then we draw more sub-pixel
    geometry that's behind closer surfaces. Visually correct, perf
    cost paid. Stage 4+5 are unaffected.

Verified pixel-identical on basic.ifc through every change. Real-scene
visual diff against GL pending the --screenshot flag on the GL minimal
(task #10's other half).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:46:13 +10:00
Gorgious56 6ec8372378 Extract bim/ifc + tool/cad helpers referenced by PR2
Fixes addon-load ImportError that surfaces when tool/geometry.py
and tool/model.py (extracted in C8 / C9) reference symbols that
don't exist on v0.8.0:

* bim/ifc.py: get_cache_or_detect_lock — IfcStore.get_cache
  variant that tracks the multi-instance-cache-locked-by-other-
  process flag, sets it on PermissionError, clears it (along with
  the dismiss flag) on subsequent success. Used by
  tool.Geometry.* to gate IFC cache reads without crashing when
  another Blender instance holds the cache lock.
* tool/cad.py: WELD_TOLERANCE constant + paired CAD helpers
  (auto-detect-curves vertex precision, polyline normal helpers,
  etc.) used by tool.Model.* + by the parametric model operators
  that land in PR4.

Both modules had zero upstream commits since the gizmos-8088 fork
point — safe bulk extraction. PR4 has no caller-line work for
either file (the additions are pure additions, no existing API
removed); the v0.8.0 callers of get_cache_or_detect_lock and
WELD_TOLERANCE are the PR2-scope files that needed them.

Generated with the assistance of an AI coding tool.
2026-05-27 11:44:01 +02:00
Gorgious56 5dc7513de0 Add tool.Blender.Modifier backward-compat shims
The previous commit moved is_<type> predicates off tool.Blender.Modifier
onto tool.Parametric, and earlier C4 moved the Array helper bag off
tool.Blender.Modifier.Array onto tool.Array. PR4 will migrate every
caller; this commit keeps the OLD entry points alive as thin delegates
so PR2 ships without breaking ~30 caller sites that still spell the
old API in v0.8.0:

* tool.Blender.Modifier.is_door / is_railing / is_roof / is_stair /
  is_wall / is_window — delegate to tool.Parametric.is_<type>.
* tool.Blender.Modifier.Array.bake_children_transform / constrain_
  children_to_parent / get_all_children_objects / get_all_objects /
  get_children_objects / get_modifiers_data / remove_constraints /
  set_children_lock_state — delegate to tool.Array.<same name>.

These shims are removed in PR5's cleanup commit once PR4 has rewritten
the call sites in bim/import_ifc.py, bim/module/geometry/operator.py,
bim/module/geometry/data.py, bim/module/model/array.py + the per-feature
operators (door, wall, window, railing, roof, stair, ui).

Generated with the assistance of an AI coding tool.
2026-05-27 09:23:29 +02:00
Gorgious56 f37c77e80c Refactor tool.Parametric — feature registry + lifecycle hooks
tool.Parametric becomes the central registry for Bonsai's parametric
features (wall, slab, door, window, railing, roof, stair, plus
mep-segment variants). Each feature registers a ParametricObject spec
declaring its enable/finish/cancel op names, props accessor, regen
callback, and is_element_type predicate.

Public surface:

* tool.Parametric.WALL / SLAB / DOOR / WINDOW / RAILING / ROOF /
  STAIR / PIPE_SEGMENT / DUCT_SEGMENT — typed accessors per feature.
* tool.Parametric.is_wall / is_door / is_window / is_railing /
  is_roof / is_stair — element-type predicates that move off
  tool.Blender.Modifier into the parametric registry. The next
  commit adds backward-compat shims on tool.Blender.Modifier so
  v0.8.0 callers keep working.
* tool.Parametric.is_object_editing(obj) — returns the registered
  feature an object is currently editing, or None.
* tool.Parametric.run_bim_op(op_name) — invoke a parametric op by
  bl_idname.
* tool.Parametric.heal_stale_edit_flags — clear is_editing flags
  on file load so a saved-mid-edit project doesn't leave gizmos
  poll-locked.
* supports_build_edit_lifecycle field on ParametricObject — declares
  whether the feature implements the build/edit/cancel triad.

The previous bare `print(f"Bonsai: commit of {obj.name!r} via
{finish_op} failed: {e}")` exception-handler is replaced with
logger.warning(..., exc_info=True). Same channel (Bonsai configures
logging to the Blender console at WARNING level), strictly more
information (full traceback), correct idiom for an error-path
message. A second logger.warning is added for parametric predicate
failures, also exception-handler scope.

Generated with the assistance of an AI coding tool.
2026-05-27 09:21:39 +02:00
Dion Moult 406124ca3d wgpu backend: HiZ occlusion culling
Stage 7 of the wgpu port. Per-frame after the main render pass:

  1. encodeHizResolve runs a depth-only render pass that samples the
     MSAA depth texture (sample 0) and max-reduces it into a small
     single-sample Depth32Float target (256 × ~h-aspect). Implemented
     as a fullscreen-triangle WGSL pipeline; one nested loop per
     output texel over its source rect. WebGPU has no built-in depth
     resolve, so this combined resolve+downsample fragment shader is
     the way.

  2. copyTextureToBuffer writes the small resolved depth into a
     CPU-mappable staging buffer (≈ 160 KB at 256×160).

  3. readbackAndBuildHizPyramid maps the staging buffer (sync via
     wgpuInstanceProcessEvents — small enough that the stall is
     well under a millisecond), strips per-row padding, and CPU
     max-reduces a full mip pyramid (level 0 → 1×1). Stores the VP
     used so the next frame can project AABBs into the same space.

Next frame, cullModelCpu calls aabbOccludedByHiz after the frustum
test: projects all 8 AABB corners through hiz_vp_, computes the
screen-space AABB and the nearest projected z, picks the mip level
where the AABB covers ≤ 2 texels per axis, samples that level's 2×2
window, and culls iff min_z > max_pyramid_depth in [0,1] z.

Plumbing changes:
  - depth_texture_ gains TextureBinding usage so the resolve shader
    can read it.
  - hiz_enabled_ master switch defaults true; mirrors IFC_NO_HIZ in
    the GL backend. Disabling skips encode + readback entirely.
  - Bench output's "hiz_rej N" field now reflects actual rejections.

Verified: basic.ifc (3 instances, no occluders) renders pixel-
identical to pre-HiZ — proves the test rejects nothing it shouldn't.
Real rejection counts need a dense scene; this should drop visible-
objects count noticeably on real BIM benchmarks where back-of-room
walls hide each other.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:16:13 +10:00
Dion Moult 6ce2b564c1 wgpu backend: per-instance contribution culling in cullModelCpu
Quick win before the proper HiZ stage. Adds a min_pixel_radius
threshold (defaults 2.0 to match AppSettings::minPixelRadius() in GL):
instances whose projected bounding-sphere radius falls below it are
dropped from the per-mesh buckets entirely.

The projected_px math (radius_world * focal_px / view_z) is now
computed once per instance and shared with the LOD pick that uses the
same number. Saves one square root per instance per frame on dense
scenes vs the previous code path that only computed it inside the LOD
branch.

Expected impact on real BIM benchmarks: visible-objects count drops by
roughly 10×, matching the GL backend's number. Without this fix, wgpu
was drawing every frustum-surviving sub-pixel instance — most of the
work and most of the geometry the GL backend wasn't even submitting.

Motion-mode threshold bump (10.0 in GL during camera drag) lands
later when mouse-driven motion tracking is wired up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:04:42 +10:00
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 a91b1da28b Reduce Rocky package size
Build Rocky artifacts with shared IfcOpenShell libraries and keep geometry writer plugins out of executable packages while preserving them for Python packages.

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 fcae3d90af Add CI workflow for Autodesk connector builds
Build the ifcviewer-autodesk connector bundle on push/PR/dispatch for the four supported targets: linux-x86_64 (ubuntu-22.04, oldest reasonable glibc), macos-arm64, macos-x86_64, and windows-x86_64. Each job runs packaging/build.py and uploads the resulting autodesk-<os>-<arch>.zip as an artifact.

Generated with the assistance of an AI coding tool.
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
Dion Moult 95c62cc70e Add Autodesk cloud sync connector
Initial implementation of the ifcviewer-autodesk connector — a separate process that bridges the IfcViewer to Autodesk APS (BIM 360 / ACC). Speaks JSON-RPC 2.0 over stdio per CLOUD_SYNC_PROTOCOL.md (also added). PKCE OAuth with keyring-backed token storage, customtkinter browse/picker UI, and PyInstaller packaging.

Implements both interactive and non-interactive variants of each push/pull (pull_ifcfed[_interactive], pull_models[_interactive], push_ifcfed[_interactive], push_model[_interactive]) so the viewer can offer both "Save"/"Open from Cloud" and "Save As"/"Add Model from Cloud" entry points. File transfers report progress through a dialog with per-byte updates; pull_models shows "(i/N)" for batches.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:19 +10:00
Dion Moult 6ca38f8bf6 Derive map units from IFC scale
Use IfcMapConversion.Scale as the source of truth for converting map coordinates to metres, instead of deriving that scale from IfcProjectedCRS.MapUnit. Bump the sidecar version because cached georef matrices and unit scales may differ under the new interpretation.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:19 +10:00
Dion Moult e0a504417c Preserve precise viewer placements
Keep placement transformations in double precision through streaming, sidecar caching, and viewport recomposition so large coordinates can be cancelled before the final GPU float upload.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:18 +10:00
Dion Moult 8b8fafa698 Unify sidecar production via SidecarBuilder
Renamed HeadlessSidecarBuilder to SidecarBuilder and reused it for live
loads. SceneLoader now constructs one per stream load, forwards meshReady
/instanceReady chunks alongside the viewport upload, and finalizes +
writes the sidecar at onStreamerFinished — no more GPU readback path
via ViewportWindow::snapshotModel (removed). Same code path now produces
sidecars for both live loads and the .rdbview offline export.

Sidecar use is opt-in per direction via SceneLoader::setShouldReadSidecar
and setShouldWriteSidecar; both default off so embedders that don't want
caching get a pure-streaming loader. ifcviewer-full and ifcviewer-minimal
opt in.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:18 +10:00
Dion Moult 5f467cd8fa Interface mockup 19 2026-05-25 16:34:18 +10:00
Dion Moult 8242efac97 Mutex bug fixed so remove hack 2026-05-25 16:34:18 +10:00
Dion Moult 22f25f0098 Interface mockup 18 2026-05-25 16:34:18 +10:00
Dion Moult fed739847e Add geometry database (.rdbview) export to IfcViewerFull
Wire a new "Export Geometry Database" tool button in AddModelDialog,
adjacent to "Convert IFC File to Database", to produce a zipped
read-only artifact combining a lossy RDB (with IfcRepresentationItem
stripped) and a .ifcview geometry sidecar. Intended for cloud
coordination workflows where parametric geometry editing is not needed.

Pipeline changes to support this:

- document_serializer_context gains a `skip_supertypes` field; the
  rdb plugin forwards it to RocksDbSerializer so the same registry
  path produces full or lossy RDBs.

- Vertex quantization helpers (octEncodeNormal + quantizeVertex) move
  out of ViewportWindow.cpp into a shared header so the sidecar's
  byte layout stays identical regardless of whether it came from a
  GPU readback or a CPU pipeline.

- New HeadlessSidecarBuilder runs a GeometryStreamer on the calling
  thread, captures MeshChunk/InstanceChunk into a SidecarData on the
  CPU, then computes georef + packed elements + LODs and writes the
  .ifcview — no ViewportWindow or GL context required.

The Controller's export flow runs RDB conversion + sidecar build +
QZipWriter packaging on a background QThread, writing through
`<dest>.tmp` then renaming for atomic appearance in cloud-sync
folders. ifcviewer-full now links Qt6::CorePrivate for QZipWriter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:18 +10:00
Dion Moult 56273de443 Add IFC to RDB conversion in IfcViewerFull
Wire the AddModelDialog "Convert IFC File to Database" button to a new
ConvertToDatabase source mode handled by ModelsPanelController, which
prompts for an .ifc input and .rdb output then runs the existing
document_serializer_registry "rdb" plugin on a background QThread with
a modal progress dialog.

Build the src/serializers subdir for BUILD_IFCVIEWER so the rdb plugin
is produced, and align serializer plugin runtime output with the
kernel/mapping plugins by writing them into $<TARGET_FILE_DIR:IfcGeom>
so default plugin discovery finds them in both dev and install layouts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:18 +10:00
Dion Moult 6901533368 Clean up IfcViewerFull naming
Rename leftover interface-era namespaces, settings, and resource identifiers inside the IfcViewerFull source tree without changing the public target name.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:18 +10:00
Dion Moult 4a91e6a87d Swap interface into IfcViewerFull
Replace the old IfcViewerFull application tree with the interface-based viewer while preserving the IfcViewerFull target and build workflow.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:18 +10:00
Dion Moult 03b8b9c1bd Refactor model settings georef view
Move model georeferencing state and rendering into a dedicated settings view, and show live IFC coordinate operation and unit data in the dialog.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:18 +10:00
Dion Moult 8f937052b8 Show ENH for first length pick
Style hidden interface models with disabled text and move the first length-tool pick coordinates to the HUD as ENH in the global georeferenced frame.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:18 +10:00
Dion Moult b1bad23a64 Add interface load progress bar
Show a real status-bar progress bar for interface model loads by wiring the shell window to SceneLoader progress and completion signals.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:18 +10:00
Dion Moult d18c05d4bd Add interface model group reparenting
Add group rename and reparenting, model-to-group moves, drag-and-drop reassignment, and clearer group creation actions in the interface models panel.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:18 +10:00
Dion Moult cee2dbcc85 Add interface sidecar writeback
Port the streamed-model sidecar writeback path into the interface, including packed element metadata, georef persistence, LOD generation, and viewport LOD application.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:18 +10:00
Dion Moult a755e96c13 Gate LOD test on meshoptimizer
Keep the IfcViewer test CMake in sync with the optional meshoptimizer dependency so test_lod_builder is only added when the package is enabled.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:18 +10:00
Dion Moult 9c1a10190d Interface mockup 17 2026-05-25 16:34:18 +10:00
Dion Moult a4f7db45cd Interface mockup 16 2026-05-25 16:34:18 +10:00
Dion Moult 6df9aeda2c Interface mockup 15 2026-05-25 16:34:18 +10:00
Dion Moult 1c598a981e ifcviewer: per-element visibility (H / Shift+H / Alt+H)
Adds VisibilityState, a CPU-only sibling to SelectionState.  It owns
the canonical hidden-id set plus a flat per-object_id byte vector that
the cull's hot path queries inline (bounds check + byte load + compare
per surviving instance).  Hidden elements never reach the visible[]
SSBO so they don't draw or pick — matching Blender/CAD convention.

ViewportWindow registers every streamed and sidecar-cached object_id
with the new state, resets it on clearScene, and connects the changed
signal to invalidate cached cull state.  Three convenience verbs:
hideSelectedElements (union into hidden), isolateSelectedElements
(replace hidden with live-object_ids minus selection, skipping
model-hidden models so element-hide doesn't pile on top of model-hide),
and showAllElements (clears the override; model-hidden models stay
hidden, per the user's spec).

Bound in the View menu: H hide, Shift+H isolate, Alt+H show all.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:18 +10:00
Dion Moult 742561c56d Interface mockup 14 2026-05-25 16:34:18 +10:00
Dion Moult 05a15e84a6 Configurable navigation bindings 2026-05-25 16:34:18 +10:00
Dion Moult a900f2c437 ifcviewer: promote runtime perf knobs, drop always-on settings
Promotes five env-var-driven knobs to AppSettings + the settings dialog
(min pixel radius, motion min pixel radius, LOD1 pixel threshold, HiZ
resolution, HiZ on/off).  Defaults: motion min pixel radius is now 10
(was 0/disabled) and IFC_HIZ_MOTION is on by default — the strict
view-projection gate reverts via env var =0 when chasing HiZ
correctness bugs.  ViewportWindow connects each *Changed signal so
changes invalidate cached cull state and take effect on the next
frame.

Removes "Load Property Data Source" and "Apply Coordinate Operation"
from the settings dialog: both are now hardcoded on.  The basic-info
property fallback (used when there's no live IFC source for an object,
e.g. .ifcview without a sibling) now triggers organically when
ElementRegistry::findEntity returns null instead of being gated on a
user toggle.  Federation::guessFederatedFalseOrigin lost its
apply_coordinate_operation parameter and now uses
georef.has_coordinate_operation directly.

src/ifcviewer/settings.rst documents the remaining diagnostic env vars
(IFC_HIZ_MOTION, IFC_CULL_THREADS, IFC_SKIP_MDI, IFC_MAX_SUBDRAWS,
IFC_FPS_HITCH_MS, IFC_SUBDRAW_DIAG, IFC_LOD_*) plus a cross-walk from
the old promoted-knob env-var names to their new QSettings keys.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:18 +10:00
Dion Moult 0a297babea ifcviewer-full: volume tool with HUD + per-object labels
Mirrors the Area tool's display: HUD shows total volume + object count,
each selected object gets a label at its world-AABB centroid showing
its individual volume.  Gated behind ToolMode::Volume (Ctrl+Shift+V) so
it stays out of the way until invoked.

Volume is a passive tool — selection works as in None (multi-select,
modifier toggle, box-select all keep working).  Area / Length still
intercept clicks through surfacePickedInTool.

Adds volumesPerObject() reusing the same mesh-cached readback path as
volumeOfObjects, so the per-object split costs no extra GL readbacks.
computeObjectAabb is promoted to public for the centroid lookup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:18 +10:00
Dion Moult bba2f30816 ifcviewer: multi-selection + box-select with active highlight
SelectionState (new) owns the multi-set, the "active" id (last single-
clicked), and a per-object_id flags SSBO bound at binding=3.  Main
shader reads sel_flags[v_object_id] for the in-set tint and a separate
u_active_id uniform for a stronger tint on the active.

Click semantics: plain replaces, Shift/Ctrl toggles.  LMB-drag past 5px
boxes the rect through a pick-pass readback — plain replaces, Shift
adds, Ctrl removes; box-select preserves the active.  Drag promotes
regardless of start point so a press on geometry doesn't disqualify it.

Sidecar fast-path bulk-loads instances, so noteObjectId is also called
from the apply path — without it the flags buffer was sized to 1 slot
while object_ids were in the 100k+ range and the in-set bit was lost.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:18 +10:00
Dion Moult d0c8c84e7b Add model coordinates settings
Add the federation/model settings dialog and related interface wiring for model coordinate configuration.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:18 +10:00
Dion Moult 7f153375b2 Rename interface modules
Move interface features from panels into modules, move AddModelDialog into the models module, and rename module Widget surfaces to Panel.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:18 +10:00
Dion Moult d25c2583da ifcviewer: fix progress bar capping at ~1/n during streaming
The streamer carved [0,100] evenly across N prioritised contexts (and
again across the net/gross passes).  In practice nearly every element
yields from the first (Body) context, so smooth progress only ever
filled range/n of the bar — typically ~20% — before snapping forward.
Drive progress directly from yielded element count over total instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:18 +10:00
Dion Moult 027e2406be Interface mockup 13 2026-05-25 16:34:18 +10:00
Dion Moult e864a961ee ifcviewer-full: per-patch area labels + skip redundant 2-pt perpendicular
Area mode now drops a label at every connected coplanar patch — a
BFS sweep over selected_ restricted to each mesh's edge adjacency
identifies the components, then each component gets one label at
its area-weighted centroid in world space.  Two clicks on
different walls now show two distinct numbers; a single BFS-grown
wall face stays one number across all its triangles.

The 2-pt length perpendicular line is now omitted when |perp|
matches any of ΔX/ΔY/ΔZ within 1mm — the surface-aligned-with-
axis case where the perpendicular is already shown by one of the
RGB legs.  Avoids redundant double-readout on axis-aligned walls.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:18 +10:00
Dion Moult d8f37b2376 ifcviewer-full: 1-pt laser, 2-pt XYZ + perpendicular, sharper visuals
Length tool's 1-pt laser is now hybrid:
  - On any surface, a coplanar BFS finds the connected face patch
    around the click and projects its vertices into the surface
    tangent basis to get an exact bounding-box extent.  Stops at
    the face edge by construction — no overshoot into adjacent
    geometry like the previous tangent-raycast did.
  - On near-horizontal surfaces (|n.z| > 0.85, i.e. floors and
    ceilings) it additionally fires one raycast in +n to the
    opposing surface — so a single floor click reports X extent +
    Y extent + ceiling height.
  - Bars are labelled by their dominant world axis (X/Y/Z) instead
    of "vertical/horizontal", which reads cleanly on either kind
    of surface.

The 2-pt readout now draws the world-space XYZ stair-step (red ΔX,
green ΔY, blue ΔZ) with each leg labelled, and a dashed
perpendicular line whenever the two picks landed on near-parallel
surfaces — useful for measuring across walls.

To support multiple line styles per frame, OverlayRenderer's
setOverlayLines takes std::vector<LineGroup> instead of a single
inline style; each group has its own color/halo/width and an
optional dash period.  The line shader gained v_along_px +
u_dash_period uniforms (screen-space dashes), and both line and
point shaders now use a sharp step() for the inner→stroke
transition with AA only on the outer halo edge — much crisper than
the previous soft band.  Default visual style trimmed: 1.5px lines
(0.5px halo), 6px dots (1px halo), opaque black halo.

Also adds ViewportWindow::raycast(origin, dir, RaycastHit&) — CPU
ray traversal of each model's per-instance BVH followed by
Möller-Trumbore against the candidate meshes' triangles (lazily
read back, cached per call).  Used by the floor/ceiling laser path
today and reusable for any future raycast-based feature.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:18 +10:00
Dion Moult acfcf6e14e Fix sidecar source loading
Treat .ifcview sources as geometry-only cache inputs, stop guessing sibling data paths, and only start data-source loading for real model sources after a sidecar hit.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:18 +10:00
Dion Moult 5bc7e4b98b ifcviewer-full: length tool (2/3/4+ point distance, angle, polygon area)
ViewportWindow trades the area_tool_active_ bool for an enum ToolMode
{None, Area, Length}; the existing surfacePickedInTool signal carries
both, the app dispatches on toolMode().  Esc exits any active tool;
Backspace/Delete in length mode emits toolBackspacePressed which the
length tool uses to remove the last point.

LengthMeasurement collects clicked world-space points and adapts the
readout: 2pt → distance + axis-aligned ΔX/ΔY/ΔZ, 3pt → angle at the
middle vertex + triangle area, 4+pt → best-fit-plane PCA + shoelace
when planar (RMS plane distance / bbox diag < 1e-3) else fan
triangulation, with the chosen method labelled in the readout.  Per-
segment lengths float at each midpoint.

OverlayRenderer grows three new pipelines to support this:
  - point sprite shader: gl_PointCoord-based outlined disc with
    fwidth-smoothed inner/stroke bands, a single draw call.
  - line shader: CPU-expand each segment to 6 verts carrying both
    endpoints + (side, along) corner index; vertex shader computes
    the screen-space perpendicular and offsets accordingly.  Real
    outlined lines independent of the driver's glLineWidth clamp.
  - screen-space rect shader: HUD + label backgrounds drawn as raw
    GL quads in NDC.  QPainter::fillRect on QOpenGLPaintDevice was
    silently dropping fills across drivers; bypassing it entirely
    via this shader makes backgrounds reliable.  Cull-face is also
    explicitly disabled here — GL_TRIANGLES respects it but the
    line/point primitives don't, so this was the one path needing
    the fix.

setOverlayLines / setOverlayPoints take an inner color, an outline
color, and an extra-pixels-per-side stroke amount.  Lines + points
draw with GL_ALWAYS so measurement annotations stay visible through
geometry; highlight tris stay depth-aware (GL_LEQUAL) so area
shading still tints the surface in place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:18 +10:00
Bruno Postle d3f0ad03fb Quote {id} placeholders in examples (issue #8101)
Shell {} expressions require quoting
2026-05-24 20:25:36 +01:00
Thomas Krijnen c8d39cc481 At least make sure that hybrid-cgal-simple-opencascade processes the elements correctly #8052 2026-05-21 14:17:17 +02:00
Gorgious56 7e96692764 Merge pull request #8089 from Gorgious56/gizmos
Parametric gizmos : Support wall and wall operations
2026-05-21 11:58:54 +02:00
Gorgious56 3e0978062f Add lifecycle-mixin tests + predicate-total registry guard
test_parametric_lifecycle.py covers the door/window/railing/roof
state-transition contracts (enable/finish/cancel; no-op on
non-matching elements; draft preserved on finish-time failure)
that the registry smoke test never exercised.

test_parametric_registry.py gains a check that every is_<name>
predicate stays total (never raises on a non-matching IFC entity)
— a raising predicate would break the save path for unrelated
types. Also rewrites the gizmo-prefs check to read __annotations__
instead of hasattr, which depended on Blender registration timing.

Generated with the assistance of an AI coding tool.
2026-05-21 11:40:38 +02:00
Gorgious56 b3f482e0fa Defer mathutils imports in stair gizmo tests
Aligns with the test/bim/ convention: heavy imports go inside test
functions so the autouse _require_real_bpy fixture skips cleanly
when bpy is mocked, rather than module-level imports failing at
collection time and erroring out the whole file.

Generated with the assistance of an AI coding tool.
2026-05-21 11:18:00 +02:00
Gorgious56 4943c77c5e Add BONSAI_TEST_ARGS env-var fallback to runpytest.py
PowerShell and some wrapper scripts on Windows occasionally strip
or reorder the `--` separator before Blender sees it, dropping the
pytest args into Blender's positional file-load slot ("File format
is not supported"). The env var carries the same args via a
shell-evaluation-free channel. Default `--` path is byte-identical
to the pre-change behaviour.

Generated with the assistance of an AI coding tool.
2026-05-21 11:17:31 +02:00
Gorgious56 6caf94f1d3 Sweep docstrings for rot-prone references
Docstrings naming sibling methods, private helpers, test files, or
historical symbols silently go wrong on rename. Strip Sphinx :meth:
/ :class: / :func: / :attr: markup that mostly added noise (no
Sphinx in this project), and rewrite five docstrings that cited
specific test paths or private hooks to describe the behaviour
instead.

Generated with the assistance of an AI coding tool.
2026-05-21 11:09:29 +02:00
Gorgious56 1e36cc318e Drop save-time parametric-edit confirm dialog
The dialog's only outcomes were "Apply & Save" (same as silent save)
or "Cancel" (same as not saving) — net friction with no actual choice.
Auto-commit stays as the safety net; the count now suffixes the
existing save-success report so it isn't immediately overwritten.

Generated with the assistance of an AI coding tool.
2026-05-21 11:00:19 +02:00
Gorgious56 46381ec08b Prioritize smaller distance gizmos in selection
When two GizmoDimension hit regions overlap (a short dimension
nested inside a longer one along the same axis), the larger one
used to win because hit boxes are scaled by world-space length —
the long box fully contains the short one, leaving the short
gizmo unreachable. The larger gizmo stays clickable at its
exposed ends, so smaller-wins is the right UX default.

Sets self.select_bias = -self._dimension_length inside
GizmoDimension.set_dimension_length. The smaller gizmo writes a
less-negative depth value in the GPU select buffer and wins the
tie-break. select_bias is unused elsewhere in the codebase, so
icon and arrow gizmos keep bias=0 and are unaffected (icons
correctly still win against dimensions, since 0 > -length).

Adds test/bim/module/drawing/test_dimension_gizmo_priority.py
with 5 cases: direct ordering, monotonicity across length ranges,
abs() handling for signed dimensions, and NaN/Inf safety.

Generated with the assistance of an AI coding tool.
2026-05-21 10:30:32 +02:00
Gorgious56 47af955dd1 Simplify pending edit popup text 2026-05-21 09:48:00 +02:00
Gorgious56 f582d0230c Fix set_icon_gizmo_position so billboard ignores object rotation
set_icon_gizmo_position computed
``mw @ (Translation @ billboard_rot @ Scale)`` — the object's world
matrix was applied AFTER the billboard rotation, so any non-trivial
object rotation (e.g. a wall rotated in plan, a stair rotated to
match a corridor) carried over into the icon's transform and tilted
it edge-on to the camera instead of facing it.

Switch to ``billboarded_at(world_pos, billboard_rot, scale)`` where
``world_pos = mw @ local_pos``: translate to world space first, then
apply the billboard rotation independently of the object's rotation.
This matches the manual pattern the base class's
``update_editing_gizmos`` already uses for validate/cancel/cycle for
exactly this reason.

Drops the now-stale workaround docstring on
``GizmoWallEdition._update_icon_row_extras`` that documented why it
bypassed ``set_icon_gizmo_position`` — the helper does the right
thing now.

Adds ``test/bim/module/model/test_stair_gizmos.py`` as the regression
guard: parametrised over six rotation angles, asserts that the rotation
part of the resulting matrix equals ``billboard_rot`` (no contribution
from ``mw``'s rotation) and that the translation lands at
``world_pos``. Also exercises ``set_icon_gizmo_position`` end-to-end via
a stub gizmo to catch the exact shape of the previously-broken call
site.

Generated with the assistance of an AI coding tool.
2026-05-20 17:28:18 +02:00
Gorgious56 26eef20eb5 Add wall parametric editing and gizmos
Walls gain in-viewport parametric editing matching the door/window/stair
UX: drag handles for length, height, slope (x-angle), layer baseline
cycle, plus cursor-anchored quality-of-life operators (split at cursor,
extend to cursor, extend height, rotate 90, toggle openings) and
two-object state-machine gizmos (unjoin / merge / join-corner /
extend-to-wall / extend-vertically / add-opening).

Wall enters tool.Parametric.EDIT_TYPES, so save-time auto-commit,
GizmoPreferencesWall registration, and the in-progress-edit predicates
all light up automatically through the registry plumbing landed two
commits back.

The three-layer commit model (drag -> BIMWallProperties -> bmesh
preview -> Finish -> single ifc.run) means dragging a handle through
hundreds of intermediate values produces zero extra IFC entities. A
no-op enable->finish round-trip is byte-identical. The snapshot diff
in FinishEditingWall skips unchanged params.
_commit_active_wall_edit_if_any ensures cursor-anchored operators see
committed geometry, not the draft preview box.

Also lands the `prompt_auto_commit_parametric_edits` BoolProperty on
BIM_ADDON_preferences (consumed by the auto-commit dialog landed in
the framework commit) and refactors
`draw_{door,window,stair}_gizmo_parameters` into a shared
`_draw_parametric_gizmo_parameters` helper that the new
`draw_wall_gizmo_parameters` reuses. This commit and the framework
commit are stacked - the framework commit references the BoolProperty
defined here, so they must land together.

Tests cover pure math (core/test_model.py), DimensionGizmoConfig text
formatter, GizmoWallExtendVertically.poll() preconditions, and the
refresh_post_commit cache-invalidation regression. BDD scenarios in
model.feature cover the edit triad, auto-commit on save, and the
two-object gizmos. Documentation added to creating_walls.rst.

Generated with the assistance of an AI coding tool.
2026-05-20 16:58:39 +02:00
Gorgious56 2143262883 Fix dead duplicates and misleading import comments
Three small post-landing cleanups against the parametric framework commit:

* core/model.py had `are_axes_collinear` and `closest_endpoint_midpoint`
  each defined twice — Python silently kept the second copy, the first
  was dead code. Removed the dead copies; runtime behavior unchanged
  (the live versions were already the kept ones).
* bim/__init__.py's `_parametric_gizmo_preference_classes` docstring
  named the wrong link in the import chain (`tool.blender → bim.ifc`).
  The real chain is `tool/ifc.py` (and ~6 other tool/* modules) which
  import `from bonsai.bim.ifc import IfcStore` at module load. Updated
  docstring to cite that root cause and the architectural fix (move
  `IfcStore` out of `bim/`).
* tool/blender.py's `from bonsai.bim.ifc import IFC_CONNECTED_TYPE`
  carried a 5-line comment claiming it was "lazy" to avoid a circular
  load. The import sits inside an `if TYPE_CHECKING:` block with
  `from __future__ import annotations` — it never runs at runtime
  regardless. Comment removed; the TYPE_CHECKING guard is
  self-explanatory.

Generated with the assistance of an AI coding tool.
2026-05-20 16:25:49 +02:00
Gorgious56 233cc344fa Add tool.Parametric registry and lifecycle mixins
Establish a single source of truth for parametric element types (door,
window, stair, railing, roof). tool.Parametric.EDIT_TYPES drives:
- BIM<Name>Properties PointerProperty attachment via the registry
- GizmoPreferences<Name> class registration in bim/__init__.py
- save-time auto-commit of pending draft edits
- the refresh_post_commit epilogue called from IfcStore after every IFC
  mutation, which fixes the stale-header bug where in-place hotkey
  mutations (S_E / C_E) left BIMModelProperties and the gizmo cache
  pointing at obsolete values.

Refactors door/window/railing/roof onto shared mixins from
bim/parametric_lifecycle.py (FeatureModifierEditMixin and
PathPreservingEditMixin); stair gets the lock-gizmo refactor and
frame-cache integration. Behavior preserved.

Adds BaseParametricGizmoGroup._prime_frame_caches so the parametric
gizmos stop re-deriving preferences, view direction, and billboard
rotation per frame; reorders poll() to short-circuit on the cheapest
predicate first. Adds the icon library + BillboardingGizmoGroupMixin
that the wall feature in the next commit will consume.

Generated with the assistance of an AI coding tool.
2026-05-20 15:18:44 +02:00
Gorgious56 a64e737d9c Merge pull request #8078 from Gorgious56/v0.8.0
Fix 8077 : Fix SHIFT + D with non-ifc object selection
2026-05-19 13:03:21 +02:00
Gorgious56 1b2507e143 Fix 8077 : Fix SHIFT + D with non-ifc object selection
When a project has a ifc file associated, selecting non-ifc objects and duplicating them with SHIFT + D now correctly both duplicate them, keep the new objects selected and starts the transform modal. IFC objects behaviour is unaffected.
2026-05-19 12:29:18 +02:00
Geert Hesselink 508b99cb73 Fix lint failures and add missing pyparsing dependency (#8048)
* unblock voxel schema loading, add test for express

* Apply black formatting

* Fix lint failures and add missing pyparsing dependency

* align ty -> 0.0.34
2026-05-18 22:17:45 +02:00
Thomas Krijnen 1cf9373000 Add test_shape_stats #8054 2026-05-18 19:25:13 +02:00
Thomas Krijnen d8c9d1a47b submodule 2026-05-18 19:24:25 +02:00
Thomas Krijnen 0993c1b0f1 Submodule 2026-05-18 18:56:37 +02:00
Thomas Krijnen 22384e136f Fix manifold kernel halfspace direction #8054 2026-05-18 18:18:05 +02:00
Thomas Krijnen 6dea7a5110 Calculate normals in manifold kernel 2026-05-18 18:16:49 +02:00
Thomas Krijnen 4e406ab1ce Change default value of assume_asset_uniqueness_by_name #8045 2026-05-18 13:29:39 +02:00
Thomas Krijnen 227d85d81f arrange polygons: limit width ratio when merging boxes 2026-05-15 21:12:43 +02:00
Thomas Krijnen a24cdf4958 Merge branch 'v0.8.0' of https://github.com/IfcOpenShell/IfcOpenShell into v0.8.0 2026-05-15 21:12:01 +02:00
Ryan Schultz e78ef865b8 Fix #8056 - Dimensions with CustomUnit" = "Inches - Fractional" should not show 0. 2026-05-15 07:28:29 -05:00
Thomas Krijnen 9345b9ce3f arrange polies: don't allow snapped point paths to cross non-containing other rect axes 2026-05-14 21:45:59 +02:00
Thomas Krijnen 0b5dded3b3 Fix temporary solution storage in arrange polygons 2026-05-14 14:37:44 +02:00
Thomas Krijnen 97218b1fdb Calculate box-width as orthogonal distance; aabb code for segment intersection (disabled) 2026-05-14 14:17:10 +02:00
Thomas Krijnen 1b637c6499 Arrange polies: reorder segment to exterior insertion based on length 2026-05-12 20:52:30 +02:00
Thomas Krijnen 424e70ac86 Remove svgfill test in pyodide wheel test 2026-05-10 20:57:46 +02:00
Thomas Krijnen fceb8911f6 Updates to workflow 2026-05-10 19:16:07 +02:00
Thomas Krijnen 94f4dbcf31 Add missing file 2026-05-10 17:01:11 +02:00
Thomas Krijnen 391f8363bb zip .so links (untested) 2026-05-10 16:54:50 +02:00
Thomas Krijnen 210861eda1 Strip to try and get back some file size increase 2026-05-10 16:54:30 +02:00
Thomas Krijnen a2012ace2e Proper svgfill isolation 2026-05-10 14:33:07 +02:00
Thomas Krijnen 05e328339f Try again with Qt install on Rocky 2026-05-10 13:44:58 +02:00
Thomas Krijnen cd612dc988 Add missing files 2026-05-09 22:00:46 +02:00
Thomas Krijnen 44ba6e8963 Untested build script updates for qt and viewer app 2026-05-09 21:59:58 +02:00
Thomas Krijnen c3a10694ba draw.py use settings instead of direct member methods 2026-05-09 21:19:55 +02:00
Thomas Krijnen da5ebf172e Delete 7za.exe from repo 2026-05-09 21:05:08 +02:00
Thomas Krijnen c1173dfc78 Additional plug-in host to try and fix arm64 build 2026-05-09 21:04:32 +02:00
Thomas Krijnen fde502daa1 svgfill as plug-in 2026-05-09 21:03:55 +02:00
Thomas Krijnen 2eb2d65710 Allow passing buffer to serializers that support it 2026-05-09 21:03:15 +02:00
Thomas Krijnen 0d8e9f0edc Explicit cast to make clang 21 happy 2026-05-09 20:28:27 +02:00
Thomas Krijnen 3c773d71d3 If it's an EMSCRIPTEN build, we're not done 2026-05-09 20:08:28 +02:00
Thomas Krijnen 43f5a79f99 Try with manifold on again 2026-05-08 20:47:16 +02:00
Thomas Krijnen 12935c83de Silent extraction 2026-05-08 20:31:29 +02:00
Thomas Krijnen e7249fe934 BUILD_IFCVIEWER=ON 2026-05-08 20:10:19 +02:00
Thomas Krijnen cf257aa20a check_installation for qt6 - was not aware of this bit 2026-05-08 19:08:08 +02:00
Thomas Krijnen 7d0b6f6fd8 Examples=Off for now 2026-05-08 17:59:38 +02:00
Thomas Krijnen 554c7174e3 Backspace everything regarding HDF5 2026-05-08 16:20:26 +02:00
Thomas Krijnen 47312e1fbb Reduce log noise on materials without styles #7947 2026-05-08 15:00:30 +02:00
Thomas Krijnen bd436765bf Win packaging changes 2026-05-08 14:05:17 +02:00
Thomas Krijnen 5270318570 Tweak Qt install handling 2026-05-08 13:40:21 +02:00
Thomas Krijnen 8fcaa18171 Tweak output names 2026-05-08 13:39:52 +02:00
Thomas Krijnen 8d0a6ecc16 Respect unicode setting for plugin debug info 2026-05-08 13:39:36 +02:00
Thomas Krijnen bfea57c617 Wire up serializer plug-ins in python 2026-05-08 10:58:09 +02:00
Thomas Krijnen 18b79a4360 Rocksdb streaming serializer connect to IfcConvert 2026-05-08 10:57:58 +02:00
Thomas Krijnen 05cd19592d Qt install (not working) 2026-05-08 10:56:41 +02:00
Thomas Krijnen a1efdccb4b Add back mutex 2026-05-08 10:07:34 +02:00
Thomas Krijnen 6ad5fbb27e Add qt6 to win build scripts using an 3rd party install script 2026-05-07 22:32:53 +02:00
Thomas Krijnen 884e7ba326 Make meshoptim optional 2026-05-07 22:31:15 +02:00
Thomas Krijnen 82f188fbe6 IfcViewer needs to be static because it does not export anything 2026-05-07 22:31:00 +02:00
Thomas Krijnen 31ddc0ecdb Respect plus-sign in versions in split_pyodide___.py 2026-05-07 22:09:00 +02:00
Thomas Krijnen 02198e3f56 Update wasm demo app for modular wheels 2026-05-07 22:08:19 +02:00
Thomas Krijnen b1899b1a8d Expand schema_plugin with schema name to eliminate symbol collisions 2026-05-07 21:10:35 +02:00
Thomas Krijnen 39d583a26a Merge remote-tracking branch 'origin/ifcviewer' into datamodel-v1.0 2026-05-07 21:01:56 +02:00
Thomas Krijnen 609b6959bc set_plugin_search_paths() inside test as well 2026-05-07 20:58:45 +02:00
Thomas Krijnen 7aa2bb366e arrange polies, fuse boxes only when obb also overlaps 2026-05-07 20:35:54 +02:00
Thomas Krijnen 2c7d25cffc Reorder .so files in wheel so that symbol dependencies do not trip up loading 2026-05-07 16:31:13 +02:00
Thomas Krijnen e893552f24 Fixes to plug-in loading in and outside of pyodide 2026-05-07 14:43:26 +02:00
Dion Moult bae1eddda9 Interface mockup 12 2026-05-07 17:10:04 +10:00
Dion Moult ad62dd6d91 ifcviewer: viewport overlay subsystem (highlight tris + HUD text)
New OverlayRenderer module owns every client-supplied overlay primitive
drawn after the main pass: tinted, depth-aware highlight triangles via
its own GL shader, and top-left HUD text via QPainter on a
QOpenGLPaintDevice.  Public surface on ViewportWindow is just two
forwarders (setHighlightTriangles, setHudText).

ViewportWindow's MeshLocalPick now exposes the instance's composed
transform so consumers can map mesh-local geometry back to world
space without re-querying.  AreaMeasurement uses both: its selection
key is now (object_id, tri) so per-instance highlighting works for
two distinct walls sharing a mesh, and on every pick it rebuilds the
world-space tri list and the HUD readout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 16:43:38 +10:00
Dion Moult d565dc3ff3 Interface mockup 11 2026-05-07 14:45:23 +10:00
Dion Moult a340e6cf9a Interface mockup 10 2026-05-07 14:17:06 +10:00
Dion Moult 359693c562 Interface mockup 9 2026-05-07 12:21:55 +10:00
Dion Moult 016c278748 ifcviewer-full: console-print accumulating coplanar-patch area tool
Adds a click-to-measure area mode triggered by Ctrl+Shift+A.  Each LMB
click expands the picked triangle into its connected coplanar patch
(BFS over shared edges, dot(normal, seed) > 0.9999); re-clicking
removes that patch; Alt+LMB skips expansion for a single triangle.
Picks across different meshes accumulate as separate patches.

ViewportWindow gains pickMeshLocalAt (screen pick → mesh-local hit
via inverse composed transform) and a tool-mode pattern mirroring
the section tool (toggleAreaTool, surfacePickedInTool signal,
areaToolToggled signal, Esc to exit).  Per-mesh adjacency is built
lazily on first pick of each mesh and dropped on tool toggle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 12:03:22 +10:00
Dion Moult f2b655fcf6 ifcviewer-full: print object volume on click via lazy GPU readback
Adds neutral primitives on ViewportWindow (readbackMeshTriangles,
findInstance) so consumers can compute per-object geometry queries
without the library retaining a CPU triangle copy. Measurement.cpp in
ifcviewer-full uses them to sum signed-tetrahedra in mesh-local space,
weighted by |det(placement_3x3)| per instance for mapped-item scaling.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 11:25:30 +10:00
Dion Moult 0bb6df6a6b ifcparse: skip flush+compact for read-only RocksDB on destruction
Read-only handles reject Flush/CompactRange, so the destructor's status
assertion always fired on shutdown when the streamer's sidecar was
opened with read_only=true. Track the flag and skip the write path; also
guard against a null db when the initial open failed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 10:16:15 +10:00
Thomas Krijnen 2257d7930a For schema plugins also wasm-opt -O1 2026-05-06 21:38:08 +02:00
Thomas Krijnen 98ff457fd6 Continue work on plug-in and tests 2026-05-06 21:17:57 +02:00
Dion Moult f07afda09c Interface mockup 8 2026-05-06 21:35:12 +10:00
Ghesselink c197a45247 Apply black formatting 2026-05-06 13:32:05 +02:00
Ghesselink ab73550059 unblock voxel schema loading, add test for express 2026-05-06 13:32:05 +02:00
Thomas Krijnen 4670715ef3 Work a bit on failing tests 2026-05-06 11:41:43 +02:00
Dion Moult 5b2721c1c4 Interface mockup 7 2026-05-06 13:13:37 +10:00
Dion Moult 0d5fa0c20c Interface mockup 6 2026-05-06 12:36:28 +10:00
Dion Moult 802ddf8ff9 Interface mockup 5 2026-05-06 10:47:03 +10:00
Thomas Krijnen ea4747ccb1 SIDE_MODULE=2 for plug-ins 2026-05-05 21:43:58 +02:00
Dion Moult e20cea221b Interface mockup 4 2026-05-05 19:59:51 +10:00
Dion Moult ae66550cae Interface mockup 3 2026-05-05 18:02:02 +10:00
Dion Moult 5ccb655e10 Interface mockup 2 2026-05-05 09:40:58 +10:00
Dion Moult 278c1e5068 Interface mockup 2026-05-05 07:29:32 +10:00
Dion Moult 095e4a1677 ifcviewer: nested groups in federation, with cascading visibility
Federation gains a nested Group tree (id, display_name, visible,
children); models reference a single group via Model::group_id.
Visibility cascades: a model is effectively visible only when its own
flag is on and every ancestor group is visible.  Persistence nests
groups directly in the JSON — no parent_id field.

ifcviewer-full surfaces this in the element tree with right-click
menus to create / rename / move / remove groups, move models between
groups, and toggle group visibility.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 17:39:28 +10:00
Dion Moult de5eb9641f ifcviewer-full: hide and remove model actions
Right-click a model root in the Elements tree to get Hide/Show and
Remove.  Hide flips the federation's per-model visible flag (already
round-tripped to .ifcfed), pushes ViewportWindow::hideModel/showModel,
and italicises + greys the tree root as a visual cue.  Remove drops
the model from the viewport, the SceneLoader (streamer + caches), the
MainWindow UI maps and tree, and the Federation — disabled while the
model is the active load.

Visibility is reapplied on each model's load completion (sidecar or
stream), so a federation saved with hidden models opens with them
hidden.  clearScene() now also drops SceneLoader state so streamers
no longer leak across federation transitions.

API additions:
- Federation::setModelVisible + modelVisibilityChanged signal
- SceneLoader::removeModel + isLoadingModel

Tests cover the setter (dirty + signal + idempotence + unknown id);
extends the existing round-trip test to actually exercise the
visibility load/save it always claimed to.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 12:22:14 +10:00
Dion Moult 4597be7fda ifcviewer: link Placement.cpp into test_federation
Commit f7add7f4 split getAxis2Placement out of an anonymous helper in
Geolocation.cpp into a shared Placement.{h,cpp}, but the test_federation
target's source list wasn't updated.  The test binary failed to link
with `undefined reference to getAxis2Placement(express::Base const&)`
from Geolocation::getWcs.  Add Placement.cpp to the explicit-source
list — it has no Qt dependency, only ifcparse.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 12:21:50 +10:00
Dion Moult ca2e866c45 serializers: skip_supertypes filter in RocksDbSerializer streaming write
Plumbed through to the Python convert_path_to_rocksdb wrapper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 07:28:12 +10:00
Dion Moult e84767c22b ifcviewer-full: multi-select directories in Add Database dialog
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 07:14:52 +10:00
Thomas Krijnen 53c2ddbb47 arrange polies: try connect to closest point when extension and projection both do not work 2026-05-03 21:46:11 +02:00
Dion Moult f7add7f412 ifcviewer: auto-guess FederatedFalseOrigin on first model added
When the user adds a model into a fresh, untitled federation that still
has the default (0,0,0, no rotation) FederatedFalseOrigin, derive an
origin from the first instance's placement_transformation (lifted
through CoordinateOperation when enabled) and the helmert grid-north
baked into ModelGeoref::coordinate_operation_meters.  Multi-file batches
naturally settle: whichever load finishes first anchors the federation,
the rest see a non-default origin and skip.  Saved .ifcfeds keep their
authoritative origin.

Adds Placement.{h,cpp} (port of util/placement.py — a2p,
get_axis2placement, get_local_placement) so Geolocation no longer needs
its own anonymous getAxis2Placement, and xaxis2angleDeg in Geolocation
mirroring util/geolocation.xaxis2angle.

SceneLoader captures the first instance's placement_transformation from
either the sidecar's InstanceCpu[0] or the streamer's first
InstanceChunk, so the guess works on both load paths without re-reading
the IFC.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 10:04:39 +10:00
Thomas Krijnen 7c6f6a4176 arrange polies performance: retain input poly provenance while subdividing; insert into arrangement_2 in batches 2026-05-02 13:21:20 +02:00
Thomas Krijnen 261037fb82 arrange polies: only subdivide segments that correspond to input poly segments 2026-05-02 13:21:20 +02:00
Thomas Krijnen eacbb55810 arrange polies: apply triangle elimination in both algo 1 and 2 2026-05-02 13:21:20 +02:00
Thomas Krijnen 3d05a5e9d1 arrange polies: lower iou to 45% 2026-05-02 13:21:20 +02:00
Dion Moult 8ffdb8f0b9 ifcviewer: cache CoordinateOperation in sidecar (v10 -> v11)
Previously, applyCoordinateOperationToViewport — which pushes both
CoordinateOperation and ModelTransformation — was only called on
paths that required the IFC source to be loaded
(onLoadedFromStream and onDataSourceReady).  Sidecar-only loads
(loadDataSource off, or no .ifc/.rdb sibling) silently lost both
stages.

Cache the per-model georef + unit scales in the sidecar itself so
the IFC source isn't needed to apply them:

  SidecarData gains
    coordinate_operation_meters[16]  // column-major
    project_length_to_meters
    map_unit_to_meters
    has_coordinate_operation

  148 B fixed block written/read between instances and elements.
  SIDECAR_VERSION 10 -> 11; existing sidecars rebuild on next load.

  MainWindow::writeSidecarForModel populates the block from
  loader_->modelGeoref(mid) before writeSidecar.

  SceneLoader::applySidecarData restores it into the model's
  ModelGeoref + sets has_georef = true, so subsequent
  loader_->modelGeoref(mid) calls return the cached data without
  needing the IFC.

  MainWindow::onLoadedFromSidecar now calls
  applyCoordinateOperationToViewport(mid) directly — both
  CoordinateOperation and ModelTransformation land at sidecar-load
  time, no longer waiting on a possibly-never-arriving data source.

Edits to the IFC's IfcMapConversion don't invalidate the cache —
delete the .ifcview manually if the source's georef changes.  This
matches the existing cache-invalidation contract.

Tests: round-trip the new fields through the existing sidecar
fixture; assert SIDECAR_VERSION == 11.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 19:08:45 +10:00
Dion Moult e0e143bab5 ifcviewer: F to frame selection from tree, debug coord dump
Tree -> viewport selection was already wired (onTreeSelectionChanged
calls setSelectedObjectId), but pressing F afterwards routed to the
focused tree widget rather than the viewport, so framing didn't fire.
Add a window-level View > Frame Selected QAction with Qt::Key_F that
delegates to ViewportWindow::focusOnSelectedObject — works regardless
of which child widget has focus.  The viewport's own F handler stays
in place for when the viewport itself owns focus.

For debugging coordinate problems, add View > Print Selected Coords
(Ctrl+Shift+P) -> ViewportWindow::printSelectedObjectCoords, which
qInfo's:
  - a sample vertex (first vertex of the selected mesh, decoded on
    demand from the quantised VBO so no extra CPU storage is needed);
  - placement_transformation (the per-instance matrix that maps the
    sample vertex from mesh-local into the model's pre-georef frame);
  - global = CoordinateOperation . placement_transformation (where
    the IFC's own IfcCoordinateOperation has been folded in);
  - the sample vertex transformed through both matrices.

The print is a no-op when nothing is selected or GL hasn't initialised.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 19:01:50 +10:00
Dion Moult 8f3eaa35d7 ifcviewer-full: per-model ModelTransformation editor
ModelTransformationDialog edits one federation model at a time.  Top
combobox picks the model; below it the form covers the four pieces
of authoring intent:

  - AFrame radio: ModelLocal vs ModelGlobal
  - Point A: 3 doubles, label switches between "model project length
    unit" and "model map unit" with the radio
  - Point B: 3 doubles in federation units (label reflects current
    FederationConfig.unit_*)
  - Rotation: rx/ry/rz in degrees, intrinsic XYZ
  - Pivot: 3 doubles in federation units

Switching models discards unsaved form edits — Ok saves the
currently-visible model, Cancel discards.  On Ok calls
Federation::setModelTransformation, which fires
modelTransformationChanged → MainWindow recomposes that model in
the viewport.

Reachable from File > Model Transformations.

End-to-end is now editable: open a federation, edit federation unit /
false origin from one dialog, edit any model's transformation from
the other, watch the viewport recompose live.  Visual verification
on a real model still pending.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 08:06:25 +10:00
Dion Moult 81b1efdfd6 ifcviewer-full: federation settings dialog (unit + false origin)
New FederationSettingsDialog edits the federation-wide unit and the
FederatedFalseOrigin (XYZ + Z-rotation in that unit).  On Ok it calls
Federation::setConfig + setFederatedFalseOrigin, which fire the
granular Federation signals MainWindow listens to → viewport
recomposes immediately.

Reachable from File > Federation Settings.  Unit picker is a fixed
combobox of common length units (metres / mm / cm / km / ft / in /
yd / mi); each item carries (prefix, name) in itemData so saving
round-trips correctly.  Per-model ModelTransformation editor still
to come — that's a per-model dialog reachable from the model entry,
not the federation-wide settings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 08:02:59 +10:00
Dion Moult 4a9af13626 ifcviewer: wire FederatedFalseOrigin / ModelTransformation to viewport
Federation grows three granular signals so consumers can recompose only
what's affected:
  - configChanged()                        — federation unit changed
  - federatedFalseOriginChanged()          — stage 3 changed
  - modelTransformationChanged(fed_id)     — stage 4 changed for one model

Emitted from setConfig / setFederatedFalseOrigin / setModelTransformation
in addition to the existing dirtyChanged.

MainWindow gains applyFederatedFalseOriginToViewport and
applyModelTransformationToViewport helpers.  Each composes the matrix
from the current federation state (using composeFederatedFalseOrigin /
composeModelTransformation, which already exist on Federation.h) and
pushes to the viewport's setFederatedFalseOrigin /
setModelTransformation.  ModelTransformation reads ModelUnits and the
active CoordinateOperation matrix from SceneLoader::modelGeoref so
ModelLocal-frame `a` lifts correctly through stage 2 when authored.

Wiring:
  - federation.federatedFalseOriginChanged -> applyFederatedFalseOriginToViewport
  - federation.configChanged               -> stage 3 + walk all models for stage 4
  - federation.modelTransformationChanged  -> stage 4 for that one model
  - applyCoordinateOperationToViewport now also re-pushes stage 4 (the
    compose result depends on the active stage 2 when a_frame is ModelLocal)
  - openFederation() pushes the loaded FederatedFalseOrigin once load
    completes; per-model stage 4 falls out of the existing
    onLoadedFromStream / onDataSourceReady path.

End-to-end pipeline is now active under the AppSettings toggle: edit
the federation in memory and the viewport recomposes immediately.  UI
for editing (form-based dialog) still pending.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 07:58:26 +10:00
Dion Moult e9d577890e ifcviewer: gate CoordinateOperation on a settings toggle
AppSettings.applyCoordinateOperation (default false, persisted via
QSettings) controls whether each loaded model's IfcCoordinateOperation
is applied at upload time.  Off keeps models in their local engineering
frame (current behaviour).  On lifts each model into map coordinates
via the stage-2 georef matrix cached on SceneLoader.

MainWindow:
  - applyCoordinateOperationToViewport(mid) reads the toggle, fetches
    the model's ModelGeoref, and pushes either the
    coordinate_operation_meters matrix or identity to the viewport.
  - Called from onLoadedFromStream (streamer path) and onDataSourceReady
    (sidecar-hit path, where the IFC arrives asynchronously).
  - Subscribed to AppSettings::applyCoordinateOperationChanged: a
    runtime toggle walks every loaded model and re-applies, so users
    can flip georef on/off without reloading.

SettingsWindow gains a "Apply Coordinate Operation" checkbox alongside
the existing per-load toggles.

Default-off so the change is opt-in — users with georeferenced models
(UTM coords etc.) can flip the toggle to see them in their map frame
once they're ready.  Visual verification on a real georeferenced
model still pending.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 07:45:40 +10:00
Richard Brice cb3253b57c Removes unnecessary operations when combining horizontal and vertical placement matrices for alignment 2026-05-01 14:13:00 -07:00
Thomas Krijnen a23cb3744f arrange polygons: debug output point and annotate self intersecting polies; fix snapping distance check and fallback; tweak max snap to exterior distance; accept non-simple polies - likely touching without edge overlap; write representative points to debug output; properly apply algo 1 fallback; correct order for halfedge elimination; 2026-05-01 16:24:20 +02:00
Dion Moult 7f29850022 ifcviewer: compose federation pipeline at SSBO upload
InstanceCpu now carries both placement_transformation (raw streamer
output, the iterator's per-shape transform with vertex-rebasing offset
folded in) and transform (the composed FederatedFalseOrigin ·
ModelTransformation · CoordinateOperation · placement_transformation
result that lands in the SSBO).  World AABBs are recomputed from the
composed transform — frustum/BVH culling sees the actual rendered
position regardless of stage state.

ViewportWindow gains:
  - ModelGpuData::coordinate_operation_meters / model_transformation_meters
  - federated_false_origin_meters_ (federation-wide member)
  - composeInstanceFromPlacement / recomposeAndUploadModel helpers
  - public setFederatedFalseOrigin / setModelCoordinateOperation /
    setModelTransformation

Each setter rewrites the affected model's SSBO, refreshes the
reflection flags, and rebuilds the BVH.  Defaults are identity, so
behaviour is unchanged until something wires a setter up — that's
the next commit (MainWindow listening to Federation::dirtyChanged
and SceneLoader::modelGeoref ready signals).

Sidecar bumped 9 -> 10: InstanceCpu grew 104 B -> 168 B.  Existing
sidecars rebuild on next load.  v10 sidecars store
placement_transformation, so they remain reusable across .ifcfeds —
the composed transform on disk is overwritten with the right one
on load.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 20:41:25 +10:00
Dion Moult 92c3b4c308 ifcviewer: rename stage1/2/3/4 to their proper IFC-mapped names
Replace the placeholder "stage1/2/3/4" terminology with names that
mirror the IFC concepts each step represents:

  stage 1 -> PlacementTransformation
            (per-instance, derived from IfcObjectPlacement)
  stage 2 -> CoordinateOperation
            (per-model, IfcCoordinateOperation / IfcMapConversion)
  stage 3 -> FederatedFalseOrigin
            (federation-wide, user-nominated)
  stage 4 -> ModelTransformation
            (per-model, user-authored within the federation)

API renames:
  FederationOrigin            -> FederatedFalseOrigin
  ModelTransform              -> ModelTransformation
  composeFederationOrigin     -> composeFederatedFalseOrigin
  composeModelTransform       -> composeModelTransformation
  Federation::setOrigin       -> Federation::setFederatedFalseOrigin
  Federation::setModelTransform -> Federation::setModelTransformation
  Federation::origin()        -> Federation::federatedFalseOrigin()
  Federation::Model::transform_intent -> ::model_transformation
  ModelGeoref::stage2_meters  -> ::coordinate_operation_meters
  ModelGeoref::has_stage2     -> ::has_coordinate_operation

JSON keys in .ifcfed renamed in lockstep:
  origin                -> federated_false_origin
  transform_intent      -> model_transformation

The streamer's per-mesh "stage 1 vertex rebasing" comment is reframed:
the rebase isn't its own stage — it's a precision optimisation applied
inside the PlacementTransformation step.

All 36 ctest cases pass under the new names.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 20:06:06 +10:00
Dion Moult 600d3a3460 ifcviewer: stage 1 mesh-vertex rebasing in the streamer
Per-mesh, when the iterator's first source vertex is more than 1 km
from origin (matching bonsai's distance_limit default), pick that
vertex as a rebase offset.  buildMeshChunk subtracts the offset from
every emitted vertex (in double precision, narrowed to float at the
end), and each instance's placement matrix is post-multiplied by
T(+offset) so world position is preserved by construction:

    T(+offset) · (verts - offset)   ≡   T · verts

The offset is stored on the per-mesh MeshAabb so all instances of the
same mesh apply the same compensation.  When the mesh's first vert is
near origin (the common case), offset is zero and the work is a no-op
beyond a couple of FP ops per vertex.

Improves float32 precision in the vertex buffer for georeferenced
models (UTM coords etc.) where verts would otherwise have to encode
million-metre magnitudes directly — at 1e6 m, float32 resolves about
6 cm, ruining sub-millimetre detail in the buildings themselves.

Visual verification on a real UTM-coords model still pending — the
math preserves world position by construction but precision claims
warrant a hand-test in the viewer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 17:39:13 +10:00
Dion Moult bea4e38e65 ifcviewer: cache per-model georef in SceneLoader
Adds ModelGeoref { ModelUnits units; Eigen::Matrix4d stage2_meters; bool
has_stage2; } and computeModelGeoref(file*) in Federation.{h,cpp}.  The
helper reads the project length unit, IfcProjectedCRS.MapUnit, helmert
parameters and WCS, and reduces them to a metres-in/metres-out stage 2
matrix using the existing Geolocation + Unit primitives.  When the model
has no IfcMapConversion it returns an identity stage_2 with has_stage2
== false, so the upload pipeline can branch cheaply.

SceneLoader::Model gains a cached ModelGeoref; SceneLoader::modelGeoref
(uint32_t mid) computes lazily on first call (returns nullptr when the
IFC file isn't available yet — happens on the sidecar-hit path before
the data-source thread populates the streamer) and serves from cache
afterwards.

Not yet consumed by the upload pipeline; that's the next commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 17:31:08 +10:00
Dion Moult 5386c9ec69 ifcviewer: add stage 3+4 data model and compose helpers to Federation
Adds the structs that were briefly in src/ifcviewer/Federation.{h,cpp}
two commits ago, now folded into the merged Federation alongside the
file persistence layer:

  - FederationConfig: federation-wide unit ({prefix, name}).  Default
    METRE; one-of an IfcSIUnit name with optional prefix or an
    IfcConversionBasedUnit name.
  - FederationOrigin: stage 3 — XYZ in federation unit + Z-rot.
    Composes to R_z · T(-xyz_meters), nominating a point as origin.
  - AFrame + ModelTransform: stage 4 intent — A (model project or
    map unit, per a_frame), B and pivot (federation unit), full
    intrinsic-XYZ Euler rotation in degrees.
  - ModelUnits: per-model project_length_to_meters / map_unit_to_meters
    cached at load time.

Free functions composeFederationOrigin and composeModelTransform
return Eigen::Matrix4d in metres.  composeModelTransform takes the
model's stage-2 georef matrix so it can lift `a` into metres when
authored in ModelLocal.

Federation gains config_, origin_ members + setters that emit
dirtyChanged.  Each Model carries a transform_intent.  JSON I/O
emits config / origin always; transform_intent only when non-default.
Schema stays "ifcfed/1" — additive, optional, sane defaults.

Five new tests: round-trip of the new fields, default-omission
behaviour, two compose smoke tests for FederationOrigin, and one
verifying the "pivot at B preserves A→B" invariant of
composeModelTransform.  All 36 ctest cases pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 15:15:37 +10:00
Dion Moult ecf0a5a4e1 ifcviewer: merge Federation classes into the lib
Move src/ifcviewer-full/Federation.{h,cpp} (and its tests) into
src/ifcviewer/ so the lib stays the single source of truth for the
federation data model.  Restores the original "agnostic lib usable
from ifcviewer-full and ifcviewer-minimal alike" framing.

Drop the unused per-model transform[16] / has_transform field — it
was round-trip-only with no UI to author it, and is being replaced
by an intent-based ModelTransform in the next commit.  No real
.ifcfed in the wild populated this field; old files still load
(unknown JSON keys ignored), they just lose the unused transform.

Replaces the pure-data-model Federation.{h,cpp} that was added a
few commits earlier — that file's structs and compose helpers
return as part of the merged Federation in commit 6.

ifcviewer-full's per-app tests dir is removed (test_federation was
the only one); BUILD_IFCVIEWER_TESTS now wires test_federation in
under src/ifcviewer/tests/, with the Qt6::Core/Gui/Test dependency
declared inline since unlike the other Tier-1 tests it has to pull
Qt in.  All 31 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 14:41:41 +10:00
Dion Moult 540f3acf52 ifcviewer: add Federation data model in Federation.{h,cpp}
FederationConfig holds the federation-wide display unit (defaults to
METRE; on load the first model's MapUnit becomes the default).
FederationOrigin captures stage 3 — XYZ in federation unit + Z-rot —
and composes to R_z · T(-xyz_meters), nominating a point as the new
origin and rotating around it.  ModelTransform captures stage 4 —
A in model project or map unit (per AFrame), B and pivot in
federation unit, full intrinsic-XYZ Euler rotation — and composes to
T(B - R_pivot · A) · R_pivot, rotating first then translating so the
rotated A lands at B.

ModelUnits caches per-model project/map unit-to-metres scales so the
compose helpers don't need to re-read the IFC each call.

All composed matrices are in metres; user-typed numbers are stored
in source units to round-trip without precision loss, and converted
on compose via Unit.h.

Not yet wired into the streamer or .ifcfed I/O — pure data model and
maths, integrated in subsequent commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 12:56:23 +10:00
Dion Moult b3d29c4081 ifcviewer: add helmertMetersFromParameters and getMapUnit
helmertMetersFromParameters builds the helmert transformation as a
meter-input/meter-output 4x4 directly from parsed parameters, bypassing
autoLocal2Global's normalisation step.  This preserves
IfcMapConversionScaled.FactorX/Y/Z in the rotation block so the factor
applies to placement translations when the matrix is precomputed
per-model and composed with placements at upload time.  For ordinary
IfcMapConversion (factor = 1) this is bit-identical to
autoLocal2Global; only diverges on rare surveyed models with non-unit
factors, where it is the only correct behaviour.

getMapUnit returns IfcCoordinateOperation.TargetCRS.MapUnit so callers
can resolve the unit-to-metres scale via Unit.h's siScaleFromNamedUnit.

autoLocal2Global is unchanged — kept as a clean port of the python
ifcopenshell.util.geolocation reference impl for one-shot
project-units-in / map-units-out callers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 12:43:27 +10:00
Dion Moult 0d3849737c ifcviewer: port unit utilities to C++ in Unit.{h,cpp}
Mirrors selected helpers from ifcopenshell.util.unit: SI prefix
multipliers, the conversion-based-unit table (foot/inch/etc -> SI
metres), siScaleFromNamedUnit (walks IfcConversionBasedUnit chains
down to IfcSIUnit), getUnitAssignment / getProjectUnit /
calculateUnitScale, and convert / convertUnit.  Lives in
src/ifcviewer/ for now alongside Geolocation; will move out when
ifcopenshell.util is ported to C++.

Needed by upcoming Geolocation fix (e/n/h on IfcMapConversion are
in MapUnit, must be converted to metres for the meter-by-default
iterator output) and by the federation module (display-unit
conversion when the user changes the federation unit).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 12:12:29 +10:00
Dion Moult a2db0a68a4 ifcviewer: port auto_local2global to C++ in Geolocation.{h,cpp}
Mirrors ifcopenshell.util.geolocation: HelmertTransformation parameters
(IfcMapConversion / IfcMapConversionScaled / IfcRigidOperation, plus
IFC2X3 ePSet_MapConversion), get_wcs from IfcGeometricRepresentationContext,
local2global, and auto_local2global.  Lives in src/ifcviewer/ for now;
will move out when ifcopenshell.util is ported to C++.

Not yet wired into the streamer.  A subsequent commit fixes the
unit handling for the iterator's meter-by-default output.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 11:58:36 +10:00
dependabot[bot] 57ef96a909 Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-01 08:58:38 +10:00
dependabot[bot] 674d98dbb3 Bump astral-sh/setup-uv from 3 to 7
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 3 to 7.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v3...v7)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-01 08:58:31 +10:00
dependabot[bot] c58711a8f7 Bump hendrikmuhs/ccache-action from 1.2.22 to 1.2.23
Bumps [hendrikmuhs/ccache-action](https://github.com/hendrikmuhs/ccache-action) from 1.2.22 to 1.2.23.
- [Release notes](https://github.com/hendrikmuhs/ccache-action/releases)
- [Commits](https://github.com/hendrikmuhs/ccache-action/compare/v1.2.22...v1.2.23)

---
updated-dependencies:
- dependency-name: hendrikmuhs/ccache-action
  dependency-version: 1.2.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-01 08:56:20 +10:00
dependabot[bot] e1a7214a29 Bump ruff from 0.15.10 to 0.15.12
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.10 to 0.15.12.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.10...0.15.12)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-01 08:56:13 +10:00
dependabot[bot] 852d620dc6 Bump ty from 0.0.29 to 0.0.32
Bumps [ty](https://github.com/astral-sh/ty) from 0.0.29 to 0.0.32.
- [Release notes](https://github.com/astral-sh/ty/releases)
- [Changelog](https://github.com/astral-sh/ty/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ty/compare/0.0.29...0.0.32)

---
updated-dependencies:
- dependency-name: ty
  dependency-version: 0.0.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-01 08:56:05 +10:00
Ryan Schultz 856631092b Fix #7885: LAYER3 crash on IfcCompositeProfileDef
The x-angle transformation for LAYER3 slabs assumed SweptArea
is always IfcArbitraryClosedProfileDef (which has OuterCurve),
but composite profiles use IfcCompositeProfileDef instead.
Apply the coord scaling to each sub-profile individually.

Generated with the assistance of an AI coding tool.
2026-05-01 08:54:02 +10:00
Ryan Schultz 7a61cf20a4 Fix #7927: Fix SECTION annotation for MODEL_VIEW drawings
generate_section_reference_points had no handler for
MODEL_VIEW target view, causing it to silently return
None. Add MODEL_VIEW branch that clips the section line
to XY camera bounds while preserving the Z coordinate
for correct 3D placement.

Generated with the assistance of an AI coding tool.
2026-05-01 08:52:55 +10:00
Ryan Schultz c999a92aa7 Fix #8024 - Fix TypeError when CardinalPoint is None
Guard the int() cast on CardinalPoint in
BIM_OT_edit_assigned_material so a None value (no cardinal
point set) no longer raises a TypeError.

Generated with the assistance of an AI coding tool.
2026-05-01 08:51:00 +10:00
E Shattow 434b179ed9 docs: project_overview: project_info blender tip to change display units after project creation
Link to Blender Manual for tip to change display units
2026-05-01 08:47:42 +10:00
Dion Moult 3e2869b6aa ifcviewer: re-enable contribution culling in ortho mode
The previous projection-toggle commit short-circuited contribution
culling when projection_ortho_ was set — the formula
r_px = focal_px * r / dist looks like it depends on per-instance
distance, which doesn't apply in ortho.  Result: every frustum-
visible object drew, including sub-pixel ones, and FPS tanked on
top-down plan views.

In ortho the projected pixel size of a bounding sphere is constant:
r_px = pixels_per_world * r, where pixels_per_world equals the
existing focal_px / camera_distance_ (the ortho box was sized to
match perspective at the pivot's distance).  So the same formula
gives the right answer if we replace per-instance dist with
camera_distance_.

cullModelCpu now does that substitution for both contributionPasses
and pixelRadius (the latter feeds LOD1 selection too — sub-pixel
objects pick LOD1 in ortho the same way they do in perspective).
The "camera inside AABB" early-return is kept; it only fires in
perspective where dist→0 would otherwise blow up r_px, and is
harmless in ortho.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 15:05:32 +10:00
Dion Moult db1a2705a3 ifcviewer: edge enhancement post-pass
Adds a per-frame depth-laplacian pass that darkens pixels at sharp
depth discontinuities — silhouettes, overlapping-surface boundaries,
section-cut edges.  Catches the wall-against-wall and slab-against-
ceiling cases that the cavity hint in the lighting shader misses.

Implementation:

- New edge_depth_fbo_ / edge_depth_tex_ — single-sample D24S8 the
  size of the window.  After the main draw, blit the default FB
  depth into it (handles MSAA resolve in the same call).
- Fullscreen triangle generated from gl_VertexID, samples four
  cardinal neighbours, computes |4c - n - s - e - w| on linearized
  depth.  Linearization branches between perspective and ortho via
  u_is_ortho.  Threshold scales with depth so distant edges still
  register.
- Output is multiplicatively blended (GL_DST_COLOR, GL_ZERO) so
  colours just darken; no separate composite step.
- Runs before the pivot/section/axis gizmos so they aren't outlined
  themselves.  HiZ pyramid build still runs after, unchanged.

Per-frame cost is one MSAA depth blit + one fullscreen pass with
five depth samples.  Sub-millisecond at 1080p on a mid GPU.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 14:34:38 +10:00
Dion Moult e7787b6aad ifcviewer: hemisphere ambient + fill light + cavity hint
Replaces the flat 0.25 ambient + single-Lambert key with three cheap
shape-readability tricks, all in the fragment shader:

- Hemisphere ambient (sky/ground tint mixed by n.z) so floors,
  ceilings, and walls get visibly different ambient colour even when
  shadowed.  +Z is world-up.
- Secondary fill light at 35% intensity from roughly the opposite
  horizontal direction so backs of objects are not pitch black.
- Cavity hint: clamp(length(fwidth(n)) * 1.5, 0, 0.35) darkens
  fragments where adjacent normals diverge sharply.  Catches
  wall-floor seams, column-slab joints, and stair edges as faint
  dark lines without any post-process.

Total cost: ~8 extra ALU ops per fragment, no extra passes, no extra
buffers.  No change to cull/HiZ/MDI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 14:22:08 +10:00
Dion Moult 6f0327d4b5 ifcviewer: orthographic toggle and standard axis-aligned views
- P toggles ortho/perspective.  The ortho box is sized so the
  visible rectangle at the pivot's distance matches what the
  perspective camera would show — toggling at any zoom keeps the
  framing identical, and the wheel keeps working by rescaling the
  box.  Contribution culling is disabled in ortho since its
  r_px = focal_px * r / dist formula assumes perspective; frustum
  and HiZ culling still run.
- X / Y / Z snap the camera to look from +X / +Y / +Z; Shift+X /
  Y / Z snap to the negative side.  Yaw and pitch are set
  directly so top/bottom land on exactly ±90°.
- updateCamera() picks the lookAt up vector dynamically: world +Z
  except within 1° of the pole, where it switches to world +Y.
  That keeps lookAt well-conditioned at the poles and gives top
  views the architectural "Y as north" screen orientation.
- Pan now derives screen-right / screen-up from the real camera
  basis instead of from yaw/pitch alone — the old derivation
  assumed up = world +Z and silently inverted at top/bottom.
- Standard views preserve target and distance — rotate only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 13:30:18 +10:00
Dion Moult 6341d7dd31 ifcviewer: bind Shift+K to clear all section planes
Convenient escape hatch when the user has stacked several cuts
and wants to start over without exiting the tool first.  Also
resets the selection and drag state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 12:37:37 +10:00
Dion Moult 51971dd493 ifcviewer: section tool — gizmo, drag, K shortcut
Wires up the user-facing section-cut tool on top of the clipping
plumbing landed in the previous commit.

- K toggles the tool.
- LMB while the tool is active:
    * On an existing plane's arrow gizmo (screen-space line-segment
      hit test, 12 px grab radius) → select + start drag.
    * Otherwise on geometry → pickSurfaceAt + addSectionPlaneAt-
      Surface, select the new plane.
    * Otherwise → deselect.
- LMB drag updates the plane's origin by projecting the cursor
  delta onto the screen-space normal axis and converting back to
  metres.  d is rederived from the new origin each frame.
- Delete removes the selected plane; Esc exits the tool.
- Each plane renders a 2x2 m quad outline plus a yellow arrow
  along +n at its origin.  Selected plane draws cyan and
  thicker.

LMB object-pick is suppressed while the tool is active so plane
creation does not also change selection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 12:32:49 +10:00
Dion Moult 309e20009c ifcviewer: add section-plane clipping plumbing
Adds a clip-plane pipeline used by the upcoming section tool:

- Up to 8 SectionPlane{n, d} entries, AND-combined as
  fragment-shader discard against world position.  Main and pick
  fragment shaders both honour the planes, so cut areas are
  neither drawn nor selectable.
- Main vertex shader now passes v_world_pos through.
- Pick FBO grows two attachments (RGB32F world position, RGB16F
  world normal) and the pick shader writes both alongside the
  object id.  pickSurfaceAt() does a single readback of all
  three.  Existing pickObjectAt() still works unchanged for
  callers that just want the id.
- addSectionPlaneAtSurface(point, normal) auto-flips the normal
  toward the camera so the first click immediately cuts the
  camera-facing half.

No UI yet — that's the next commit (gizmo, drag, K shortcut).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 12:13:17 +10:00
Dion Moult a06e5ecdf5 ifcviewer: add Focus-on-Object and View-All camera shortcuts
F (no modifier) re-aims the orbit camera at the selected object's
world AABB centroid and dollies camera_distance_ so the bounding
sphere fits the current viewport.  Home does the same for the union
of all finalized models.  Both preserve yaw/pitch so the user keeps
their orientation; both no-op in FPS mode.

Scene AABB prefers the per-model BVH root when available and falls
back to walking InstanceCpu world AABBs.  Object AABB unions every
matching instance.  Distance accounts for portrait windows by using
the tighter of the horizontal and vertical FOV constraints.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 11:34:35 +10:00
Dion Moult 1ca2e12f92 ifcviewer: draw 3D pivot indicator during navigation
A small RGB axis cross is rendered at camera_target_ while the user is
orbiting, panning, or has just zoomed.  Visibility toggles on
middle-mouse press/release; the wheel arms a single-shot QTimer that
hides it 750 ms after the last notch.

Drawn in two passes: GL_GREATER at 30% alpha for the occluded portion
(X-ray cue) and GL_LEQUAL at full alpha for the visible portion.  Arm
length is computed from camera_distance_, fovy, and viewport height so
the cross stays ~30 px on screen across zoom levels.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 10:21:23 +10:00
Dion Moult b9e5739088 ifcviewer: stream geometry per prioritised context
Port get_prioritised_contexts from ifcopenshell.util.representation to
C++ and have GeometryStreamer iterate one context at a time, mirroring
bonsai's create_generic_element loop.  Each pass sets context-ids to a
single context id; elements that yield geometry are dropped from the
include set so lower-priority contexts only pick up leftovers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 23:00:33 +10:00
Dion Moult 30cdffc27a ifcviewer: include settings for deflection tolerances 2026-04-29 22:37:55 +10:00
Dion Moult 3607fbb762 ifcviewer: include spatial elements in iterator filter
Match bonsai's process_element_filter for the no-filter branch:
IfcSpatialStructureElement on IFC2X3, IfcSpatialElement otherwise.
They flow through the same net/gross split as IfcElement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 22:20:20 +10:00
Dion Moult b73cdd1a0e ifcviewer: filter iterator to net IfcElements, void-limit setting
Mirror bonsai's IfcImporter.process_element_filter so the streamer
walks only IfcElement (plus IfcProxy on IFC2X3/IFC4), drops
IfcFeatureElement except IfcSurfaceFeature, and routes elements
with more openings than the configurable void limit through a
second iterator pass with disable-opening-subtractions=true.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 08:54:35 +10:00
Dion Moult e21bd1ac96 ifcviewer: add tier-1 unit tests (Catch2 + CTest)
Covers the pure-logic modules with no Qt event loop or GL context: BVH
build, LOD decimation, sidecar round-trip, instanced-geometry layout
constants, and Federation save/load + relative-path policy. Each test
binary compiles only the production source(s) under test, so the unit
tier doesn't pull Qt/OpenCASCADE/IfcGeom into the test build.

Gated behind BUILD_IFCVIEWER_TESTS=OFF; default builds remain offline.
Catch2 v3.5.4 is fetched on demand via FetchContent.
2026-04-28 21:49:44 +10:00
Dion Moult 633c613da2 ifcviewer: drop unused SidecarHeader reserved field, bump v8 -> v9
The reserved uint32_t was always written as 0 and never inspected on
read.  Removing it shrinks the header from 16 to 12 bytes; the version
bump makes pre-existing sidecars fail the version check cleanly rather
than misreading by 4 bytes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:40:00 +10:00
Dion Moult 8282f691e8 ifcviewer: rename SceneLoader::Entry to Model
The struct holds per-model bookkeeping; Model describes its contents
rather than its container relationship.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:31:48 +10:00
Dion Moult 7bc64bef1a ifcviewer-full: add .ifcfed federation save/load
Federation (JSON) tracks an ordered list of model sources plus an
optional home-view camera state. Sources are stored relative when
under the federation file's directory, absolute otherwise.

File menu now exposes New / Open / Save / Save As; Add Files moves
to Ctrl+Shift+O. View menu gains Set/Go to Home View. Window title
binds to dirty state via setWindowModified, and the close-window
prompt offers Save/Discard/Cancel.

Per-model transform (4x4 column-major) and visible round-trip
through load/save but are not yet applied at the viewport — the
georeferencing work uses them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 18:31:59 +10:00
Thomas Krijnen 8b5b4006aa Try with manual paths 2026-04-26 21:29:16 +02:00
Thomas Krijnen 98c24b95f3 Simple SPF submodule update 2026-04-26 21:28:02 +02:00
Dion Moult 81a7c5b50e ifcviewer: add Shift+F fly-mode camera
WASD strafe, Q/E down/up, mouse-look (cursor hidden + recentered),
Shift to sprint, scrollwheel scales speed, click or Esc returns to
orbit.  Exiting drops back to the same viewpoint because rotation
re-pins camera_target_ to keep camera_eye_ stationary.

Movement integrates wall-clock dt inside render() and the next frame
self-schedules via requestUpdate() while any key is held.  A QTimer
would fight Qt's event loop during long swapBuffers blocks and produce
"camera pauses one frame" stalls; render-driven integration keeps
movement phase-locked to vsync and absorbs slow frames in a single
catch-up step.

IFC_FPS_HITCH_MS=<n> logs frames slower than n ms while in fly mode.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 20:12:52 +10:00
Thomas Krijnen 3b3ed47e89 Reduce concurrency to see if we can get the rc 143 to go away 2026-04-25 14:34:33 +02:00
Thomas Krijnen f54c917ded Try to disable wasm-opt 2026-04-25 14:09:09 +02:00
Thomas Krijnen 5c6444d5ea [tmp] disable manifodl 2026-04-25 11:41:43 +02:00
Thomas Krijnen 6282634a70 Try with manual paths 2026-04-25 11:34:31 +02:00
Thomas Krijnen 33809c7266 pin pyodide versions 2026-04-25 11:15:14 +02:00
Thomas Krijnen 40267aa068 Revert some tmp changes 2026-04-25 11:13:56 +02:00
Thomas Krijnen 1efedfd3cc pin pyodide versions 2026-04-25 11:13:48 +02:00
falken10vdl 247a445458 Fix IfcSurfaceStyleRendering colour reset on save 2026-04-25 16:15:43 +10:00
Thomas Krijnen 421fab45f3 Update build_pyodide.sh to source emsdk_env.sh conditionally
Add conditional sourcing for emsdk_env.sh
2026-04-24 14:28:45 +02:00
Thomas Krijnen 57982a0d99 arrange_polygons: Revert to unsimplified when big IoU difference; threshold on max snap distance; write most deviating input-output pair to debug output 2026-04-24 14:10:26 +02:00
Thomas Krijnen ddfe3bce20 Fixes for WASM build (some temporary) 2026-04-24 13:36:06 +02:00
Dion Moult 8f7c8dc1d2 ifcviewer: load rdb/ifc as property data source on sidecar hit
Sidecar hits skipped opening the underlying .rdb/.ifc, so ifcFile() was
null and the property panel only showed cached name/type/guid. Now, after
a sidecar hit, a background thread opens <stem>.rdb (preferred) or
<stem>.ifc and hands the file to GeometryStreamer via setIfcFile(), with
a dataSourceReady signal so the UI refreshes the current selection.

Gated behind a new AppSettings::loadDataSource toggle (default on) so
users can opt into geometry-only viewing; when off, the sidecar-hit
thread is skipped and the stream-path ifc_file_ is released after
the sidecar write completes.

Also adds *.ifcview to the Add Files dialog filter so a cache can be
opened directly without its source file present.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 13:45:04 +10:00
Dion Moult eea2398e07 ifcopenshell-python: fix broken imports after upstream refactors
Two upstream commits on this branch landed without updating all their
callers, leaving `import ifcopenshell.geom` unusable:

  89c66f62b "Python import fixes: import from wrapper now which
  inherits from mixins" moved the `file` class out of
  ifcopenshell/file.py into ifcopenshell_wrapper, but missed
  geom/main.py and stream.py which still did `from ..file import file`.

  b022ca7e7 "Some plug-in work" dropped the SWIG exports for
  `serialise`, `tesselate`, `XmlSerializer` (and other serializers)
  with a `// @todo bring back serialization` marker, but left
  geom/main.py referencing them at module-load time.

Fix the `file` imports to come from ifcopenshell_wrapper, and guard
the removed-serializer references behind `hasattr`, matching the
pattern already in use for the other optional serializers (gltf, hdf5,
collada, json, ttl). Revert once upstream fixes this.
2026-04-24 07:33:13 +10:00
Richard Brice c39fe6e8a3 Fixes bug in addRelatedObject<> for IfcRelReferencedInSpatialStructure 2026-04-23 08:40:05 -07:00
Dion Moult 35be7f4190 ifcviewer: load RocksDB-backed IFC models
The viewer can now open a .rdb directory (as produced by
RocksDbSerializer / convert_path_to_rocksdb) anywhere it accepts an
.ifc file. The full GUI gets an "Add Database..." File menu entry
that opens a directory chooser; the streamer lets the file
constructor autodetect the format and opens the store read-only so
multiple viewers can share a database without taking the exclusive
RocksDB lock.

Parallel mapping on RocksDB-backed files still produces
non-deterministic shape counts (the race is outside the instance
cache), so force num_threads=1 for the iterator when the storage is
RocksDB. Serial RocksDB (~2.6s) and parallel SPF (~0.7s) both
produce 107 shapes on AC20-FZK-Haus; @todo in-source points at the
remaining thread-safety work.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult 4f929e90a7 ifcviewer: key sidecar on path stem, drop staleness check
Previously readSidecar/writeSidecar were keyed on (path, file_size) with
staleness rejected at read time.  Switch to pure path-stem keying: foo.ifc
and foo.ifcdb/ both resolve to foo.ifcview, so the same cache serves either
source format.  Staleness is user-managed (delete the sidecar to force a
rebuild), which also lets sidecars be copied or moved independently of the
source.

v8 header drops the source_file_size field.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult f898089b27 Queue viewer ops before GL init
Buffer viewport model mutations until the OpenGL context is initialized so loads that start before first exposure do not silently drop geometry or model state.

Generated with the assistance of an AI coding tool.
2026-04-23 21:32:25 +10:00
Dion Moult 4e4553201b Fix viewer load termination
Handle streamer success, failure, and cancellation as distinct terminal states so failed or cancelled loads do not finalize as successful models. Clean up partial model/UI state in the full and minimal viewer apps when a load is cancelled or fails.

Generated with the assistance of an AI coding tool.
2026-04-23 21:32:25 +10:00
Dion Moult 29f5132510 ifcviewer: extract SceneLoader, remove duplicated load orchestration
MainWindow and MinimalWindow each carried ~150 lines of mirrored
load-queue, sidecar-thread, streamer-wiring, and ID-rebase code. Lift
all of it into a SceneLoader QObject in the library; both apps now
consume it via signals. Sidecar writes stay on the full-app side since
they need the consumer's element metadata strings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult ae938d425b ifcviewer: split into shared library and two app executables
Turn src/ifcviewer into libIfcViewer.so holding the rendering engine +
geometry pipeline (ViewportWindow, GeometryStreamer, BvhAccel,
InstancedGeometry, SidecarCache, LodBuilder, AppSettings).  Move the
existing UI shell (MainWindow, SettingsWindow, main.cpp) into
src/ifcviewer-full as the IfcViewerFull executable.  Add a new
src/ifcviewer-minimal target with a MinimalWindow that hosts only the
viewport and reuses the sidecar fast-path for benchmark/debug runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult 5161b0a3f8 ifcviewer: remove meshopt_simplify path, keep only simplifySloppy
Edge-collapse decimation (meshopt_simplify) returns BIM meshes unchanged
due to per-triangle vertex duplication and non-manifold topology. The
sloppy voxel-clustering decimator is faster, needs no shadow index
welding, and produces good results at the sub-30px LOD1 threshold.
Remove the non-sloppy branch, shadow buffer, IFC_LOD_SLOPPY and
IFC_LOD_LOCK_BORDER env vars.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult 3015f758ba ifcviewer: shrink vertex format from 16 to 12 bytes (oct i8x2 normals)
Replace i16x2 octahedral normals with i8x2, filling the 2-byte padding
after position and saving 4 bytes per vertex. int8 gives ~1.4 deg
worst-case angular error — invisible for BIM geometry which is
overwhelmingly axis-aligned. 25% VBO reduction; sidecar files shrink
~15% overall (5.4 GB -> 4.6 GB on a 111-model test scene). Bumps
sidecar format to v7.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult dbe68b48f2 Fix kernel/mapping plugin output dir and IfcViewer link dependencies
Use $<TARGET_FILE_DIR:IfcGeom> instead of hardcoded
${CMAKE_BINARY_DIR}/ifcgeom/$<CONFIG> for plugin runtime dirs — the
old path was wrong on non-MSVC generators where $<CONFIG> expands
empty. Add explicit add_dependencies for kernel/mapping plugins so
IfcViewer waits for them to build, and drop the redundant direct link
against ${kernel_libraries}.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult 094d96c735 ifcviewer: remove GPU compute cull (IFC_GPU_CULL)
Benchmarks showed negligible gain (52 vs 51 fps) — the CPU BVH path
already culls efficiently, and the GPU path still read back to CPU for
LOD/winding/HiZ. Removes ~570 lines of dead weight: compute shader,
async readback, one-frame-late consume, per-model AABB SSBOs, and
profiling counters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult ed6e8d831e ifcviewer: benchmark CLI, settle recull fix, and Phase 3G documentation
Add --camera tx,ty,tz,dist,yaw,pitch and --benchmark N CLI args for
reproducible performance measurement.  The benchmark orbits the camera
(0.5°/frame yaw) for N frames after a 5-frame warmup, prints
avg/median/p1/p99 frame times, then exits.  Press C during interactive
use to print the current camera as a --camera argument.

Fix settle recull to fire after ANY camera motion (not just when
IFC_MIN_PX_MOTION is set), ensuring HiZ artifacts from motion frames
are always cleared when the camera stops.

Document Phase 3G (motion-adaptive culling + HiZ during motion) in
README with benchmark results from 1.06M-instance scene:
  - Baseline:                    16.3 fps
  - IFC_MIN_PX_MOTION=10:       26.5 fps (1.6x)
  - IFC_HIZ_MOTION=1:           46.6 fps (2.9x)
  - Both combined:              51.0 fps (3.1x)
  - + GPU_CULL:                 52.0 fps (3.2x, negligible gain)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult 930678e3d2 ifcviewer: motion-adaptive contribution culling + sub-draw diagnostics
During camera motion, use a larger pixel-radius threshold (IFC_MIN_PX_MOTION)
to aggressively cull small objects, dramatically reducing sub_draws and
improving orbit fps (e.g. 29→67 fps on 1M-instance scene).  When the camera
stops, automatically re-cull at the base threshold to restore full detail.

Key behaviors:
- IFC_MIN_PX_MOTION=N sets the motion threshold (0 = disabled)
- Settle recull fires on the first still frame after motion
- HiZ pyramid invalidated on settle (stale from sparse motion frame)
- GPU cull results skipped on settle (dispatched at motion threshold)
- requestUpdate() ensures the settle frame actually runs

Also adds IFC_SUBDRAW_DIAG=1 diagnostic for sub-draw composition analysis
and documents Phase 3E/3F experiment results in README.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 4e3cc63de1 ifcviewer: fix HiZ depth blit and make occlusion test conservative
The HiZ pipeline had two bugs causing false occlusions:

1. The scaling depth blit (glBlitFramebuffer from window-size to HiZ-size)
   produced GL_INVALID_VALUE on some drivers. Replace with a fullscreen-
   triangle shader that samples the resolved depth and writes gl_FragDepth.

2. The resolve texture used GL_DEPTH_COMPONENT24 but Qt's default FBO uses
   D24S8 (depth+stencil). Mismatched formats cause the MSAA resolve blit
   to fail. Fix by using GL_DEPTH24_STENCIL8 for the resolve texture.

Additionally, the occlusion test was too aggressive for scenes with
compressed depth ranges (entire scene in 0.99-1.0). Change from
"max over coarse mip texels" to "reject only if ALL fine-mip texels
agree the AABB is behind them", with early-out on first non-occluding
texel and a 64-sample cap.

Also fix IFC_HIZ_MOTION=0 being treated as enabled (checked env var
existence, not value).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 7b64dd338b ifcviewer: dirty-mesh tracking + consume sub-phase profiling for GPU cull
Only clear and emit mesh buckets that received survivors in the previous
frame, converting both phases from O(total_meshes) to O(active_meshes).
Adds per-sub-phase timing (bin/clr/class/emit) to the stats line.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 71612e0780 ifcviewer: hybrid GPU frustum+contribution cull with async readback
Replace the CPU BVH traversal + frustum + contribution stages with a
GPU compute path (IFC_GPU_CULL=1).  A single scene-wide dispatch tests
all instances against frustum planes and screen-space contribution
threshold, compacting survivors into a flat uint32 buffer via atomicAdd.

Uses one-frame-late async readback: frame N dispatches and fences,
frame N+1 polls the fence (non-blocking) and reads the persistent-
mapped result buffer with zero GPU sync cost.  CPU still handles HiZ,
LOD selection, winding bucketing, and indirect command generation from
the compact survivor list; draw path is unchanged.

On a 1M-instance / 111-model scene (GTX 1650):
  GPU dispatch:  0.70 ms  (frustum + contribution, brute-force)
  Readback:      0.00 ms  (fence already signaled, persistent map)
  CPU consume:   5.7–6.7 ms  (parallel emit across models)
  Cull wall:     5.8–6.9 ms  (vs 9.6–15.2 ms CPU-only path)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 9aae8f0329 Revert "ifcviewer: GPU cull drives rendering under IFC_GPU_CULL=1"
This reverts commit 4fe32b54105ca2c5c00290603db17164837211e1.
2026-04-23 21:32:24 +10:00
Dion Moult 175efcfffe Revert "ifcviewer: GPU cull fwd/rev reflection bucketing (step 3b)"
This reverts commit 7defbe982464536e34e80aa85d2cd7eaafbb62ee.
2026-04-23 21:32:24 +10:00
Dion Moult 643a2e1c1f Revert "ifcviewer: GPU LOD0/LOD1 selection in compute cull (step 3c)"
This reverts commit 77cac3ec170b622db6977829f66b62603266a047.
2026-04-23 21:32:24 +10:00
Dion Moult 3c5c8e44cb Revert "ifcviewer: same-frame HiZ occlusion cull on GPU (step 3d)"
This reverts commit 9a7a48944f4b62f9ca431149139eb846229f6114.
2026-04-23 21:32:24 +10:00
Dion Moult fc89ffeb19 Revert "ifcviewer: MDI compaction via glMultiDrawElementsIndirectCount"
This reverts commit d5b7b87ba17c90008cf0673c838ce8431ad85e36.
2026-04-23 21:32:24 +10:00
Dion Moult 4bedb40d8a ifcviewer: MDI compaction via glMultiDrawElementsIndirectCount
Pack compute shader compacts non-empty indirect commands into
contiguous fwd/rev ranges, eliminating ~690k empty sub-draws that
dominated command-processor overhead.  GL 4.6 entrypoint loaded via
getProcAddress with ARB fallback; graceful degradation to uncompacted
MDI when unavailable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult a34c36d22e ifcviewer: same-frame HiZ occlusion cull on GPU (step 3d)
Two-phase compute-cull dispatch when IFC_GPU_CULL=1:

  Phase 1  frustum + contribution + LOD, no HiZ  → survivors
  Depth    render survivors depth-only into half-viewport FBO
  Build    GPU compute max-reduce depth → R32F mip pyramid
  Phase 2  same cull + HiZ test                  → final survivors
  Color    render final survivors

The compact shader's new hizOccluded() projects 8 AABB corners to
screen space, picks the mip level where the covered rect fits in ≤2×2
texels, and rejects when the AABB's near-depth exceeds the pyramid's
max depth.

New GPU resources (per-window):
  hiz_gpu_fbo_ / hiz_gpu_depth_tex_  — depth-only FBO at half viewport
  hiz_gpu_pyramid_tex_                — R32F mipmapped pyramid
  hiz_gpu_copy_prog_                  — compute: depth → pyramid L0
  hiz_gpu_reduce_prog_                — compute: max-reduce L(n-1)→L(n)
  hiz_gpu_depth_prog_                 — vertex + trivial fragment

On a dense 18-model BIM dataset:
  survivors:  140k → 65k  (HiZ rejects ~50%)
  triangles:  22M  → 13M
  gpu_cull:   0.06ms → 22.5ms  (depth pre-pass CP overhead)

The depth pre-pass suffers the same empty-sub-draws CP overhead as the
color pass (690k commands, most with instanceCount=0).  Once MDI
compaction lands, both passes will be fast.  For now, net FPS is flat
(savings on color ≈ cost of depth pre-pass).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult e5ed7b53d4 ifcviewer: GPU LOD0/LOD1 selection in compute cull (step 3c)
The compact shader now computes per-instance pixel radius and routes
survivors to LOD1 buckets when the projected sphere falls below the
LOD1 threshold (default 30 px, same as CPU path, tunable via
IFC_LOD1_PX).

Layout expanded from 2 to 4 buckets per mesh:
  [0..M)   fwd_lod0   [M..2M)   fwd_lod1
  [2M..3M) rev_lod0   [3M..4M)  rev_lod1

Two MDIs per model: CCW for [0..2M), CW for [2M..4M).  Per-mesh
has_lod1 flags live in a new gpu_mesh_flags_ssbo (binding 4).

Contribution cull refactored: the compact shader now computes
pixelRadius() once and uses it for both the min_pixel_radius rejection
and LOD routing, matching the CPU path's logic.

Visible-buffer worst case is 2 × total_instances (each LOD bucket
reserves the full fwd/rev capacity per mesh, since LOD selection is
dynamic).

Tri count drops ~60% on the test dataset (53M → 22M) thanks to LOD1
decimated meshes.  FPS recovers from 16 to 36 despite 690k sub_draws
(4M layout).  MDI compaction remains the final perf fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 069ef20c46 ifcviewer: GPU cull fwd/rev reflection bucketing (step 3b)
Extend the GPU-cull indirect buffer from M to 2M commands: the first M
are the forward (non-reflected, CCW) bucket, the second M are the
reverse (reflected, CW) bucket.  The compact shader reads flags bit 0
from the AABB SSBO and routes each survivor to the appropriate bucket
via bucket = reflected ? mesh_id + M : mesh_id.

uploadGpuCullStaticBuffers() now precomputes exact per-mesh fwd/rev
instance counts so each bucket reserves only the slots it needs
(total visible_ssbo size unchanged — sum of fwd + rev = total).

Draw loop issues two MDIs per model under IFC_GPU_CULL: first M
commands CCW, next M commands CW.

Sub-draws doubled (172k → 345k) which further regresses FPS due to
command-processor overhead from zero-instance sub-draws — the same
issue noted in 3a.  MDI compaction remains the fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 0b122ae1f5 ifcviewer: GPU cull drives rendering under IFC_GPU_CULL=1
Promote the compute cull from a validation shader to the actual draw
driver.  With the gate on, the CPU cull fan-out is skipped and MDI
consumes gpu_indirect_buffer / gpu_visible_ssbo directly.

- uploadGpuCullStaticBuffers() pre-fills per-mesh DrawElementsIndirect
  commands and a mesh_base prefix sum so the compact shader can scatter
  survivors into a fixed per-mesh range.  Instance count for each
  command is zeroed by a tiny reset dispatch, then the compact shader
  atomically writes survivors and increments instanceCount.
- Draw loop branches on the gate: single CCW MDI with all mesh
  commands.  Fwd/rev winding split, LOD selection, and HiZ are still
  CPU-path-only; reflected instances render with wrong winding under
  this gate (step 3b).
- Once-per-second readback of each model's indirect buffer populates
  the survivor / visible-object / visible-triangle stats so the
  [frame] line reflects what the GPU actually drew.

Known regression: sub_draws is the full mesh count per model (~172k on
the test dataset) vs the handful of non-empty commands the CPU path
produces.  Command-processor overhead from zero-instance sub-draws is
what drives the FPS drop, not the cull itself (0.05 ms).  Compacting
non-empty commands requires glMultiDrawElementsIndirectCount, a GL 4.6
entrypoint not exposed by Qt's QOpenGLFunctions_4_5_Core; deferring to
3a-followup so we don't bolt a getProcAddress loader into the renderer
mid-restructure.

IFC_GPU_CULL is off by default, so this does not affect normal runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult a0cc4b874b ifcviewer: add GPU frustum-cull validation shader (IFC_GPU_CULL=1)
First Phase 3E milestone: a compute shader that reads the per-instance
world-AABB SSBO added in the last commit, tests each instance against
the 6 frustum planes, and atomicAdds a global counter.  No visible list
or indirect-buffer writes yet — the output is just a survivor count,
cross-checked each frame against the CPU cull's numbers in the stats
line (`gpu_cull[Xms in=A surv=B]`) so we can verify the plumbing end-
to-end before we hand the GPU responsibility for the actual render data.

Dispatched from render() after the CPU cull completes, only when
IFC_GPU_CULL=1 and the camera moved (the skipped-cull still-frame path
doesn't re-check either).  The readback is synchronous — that's fine
for a validation path; it'll go away once the GPU writes indirect
commands directly.

Expected invariant: gpu_cull.surv >= cpu_cull.visible_objects, since
the GPU path does frustum-only and CPU adds contribution + HiZ cuts on
top.  A large mismatch (orders of magnitude, or surv < visible) means
the SSBO upload or shader logic is wrong.

No shader/buffer bindings overlap with the draw path (compute uses
bindings 0/1, restored before drawing; draw programs rebind 0/1/2).
2026-04-23 21:32:24 +10:00
Dion Moult 2f88778c9f ifcviewer: upload per-instance world AABBs to a GPU SSBO
Scaffolding for Phase 3E (GPU compute cull).  After finalizeModel /
applyCachedModel, pack each InstanceCpu's world AABB + mesh_id +
reflection bit into a std430-friendly 32 B record and push it to a
per-model aabb_ssbo.  No consumer yet — the CPU cull still drives
rendering — but the next commits will point a compute shader at this
buffer and have it produce the visible list + indirect commands
directly on the GPU.

Cost: 32 B per instance, ~18 MB for the 569 k-instance test scene.
One-shot upload at finalize time; streaming-time appends aren't
mirrored (the CPU cull doesn't need the SSBO, and finalizeModel
rebuilds the whole thing in one go).
2026-04-23 21:32:24 +10:00
Dion Moult 0a752e09eb ifcviewer: README — document HiZ disabled during camera motion
The 'Known caveats' bullet still described the old 1-frame-stale
behavior.  Since 6b496d802 the cull compares hiz_vp_ to the current VP
and drops HiZ rejection whenever they differ, so HiZ only helps on
still frames — orbiting gets no benefit.  Call out the tradeoff and
the planned same-frame-depth-pre-pass fix slated for Phase 3E.
2026-04-23 21:32:24 +10:00
Dion Moult 03662d2016 ifcviewer: fix pick-pass cull corruption and cached-model ID collisions
Two stability bugs:

1. Clicking an object left the scene with wrong shading until the camera
   moved.  The pick pass re-culls every model with its own parameters
   (min_pixel_radius=0, no HiZ) and overwrites each model's visible_ssbo
   and indirect buffer.  The next render() saw an unchanged camera,
   skipped the cull via the have_cached_cull_ shortcut, and drew the
   stale pick-pass buffers.  Fix: invalidate have_cached_cull_ at the
   end of pickObjectAt().

2. Loading two sidecar-cached models made the second model's picked
   properties resolve to the first model's elements.  Sidecars store raw
   object_id / model_id values from the session that wrote them, and
   both files start at object_id=1, so element_map_ entries collided.
   Fix: on load, rebase every PackedElementInfo and InstanceCpu by
   (next_object_id_ - min_id_in_sidecar) and overwrite model_id with
   the freshly-assigned handle before the elements hit element_map_.

Also document both in the README — the pick-pass note under 3A
contribution culling, the sidecar rebase under the sidecar format
section.
2026-04-23 21:32:24 +10:00
Dion Moult 99f409280a ifcviewer: disable HiZ cull when camera has moved
HiZ from last frame encodes depth from last frame's viewpoint. When
the camera moves, projecting a current-frame AABB through the stored
VP answers 'was this occluded last frame?' rather than 'is it occluded
now?' — a self-reinforcing feedback loop where objects culled in
prior frames never appear in any depth buffer and stay permanently
hidden at certain camera angles.

Fix: require hiz_vp_ == current VP for the HiZ test to apply. HiZ
still helps static views (kicks in one frame after camera stops) but
no longer produces false occlusions during orbit. The correct fix for
orbit coverage is a depth pre-pass feeding fresh HiZ — planned as
part of Phase 3E GPU compute cull.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 0c9d3ea6d7 ifcviewer: README — document parallel per-model cull (Phase 3D)
Add the parallel cull bullet to the feature list, a Phase 3D section
explaining the fan-out / scratch-ownership design + measured 4x
speedup, and renumber the planned GPU compute cull to Phase 3E so it
can cite 3D as the CPU algorithm being ported.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 37fa4e9076 ifcviewer: parallel per-model CPU cull
Split cullAndUploadVisible into cullModelCpu (CPU-only, thread-safe) and
uploadCullResults (GL-only, main thread). render() fans the per-model
culls out via std::async and joins before the serial upload pass.

The cull scratch (vis_fwd/rev_lod0/1, visible_flat, indirect_scratch)
moved onto ModelGpuData so each worker owns its output buffers. Phase
timers and hiz_reject_count_ are atomic since workers fetch_add into
them. A new wall-clock timer around the dispatch block reports the
actual frame-time contribution; the existing clr/trv/emt counters are
now documented as per-thread sums.

Measured on the 18-model / 569k-instance test scene: wall-clock cull
dropped from ~25 ms to ~5 ms while the aggregate CPU work (trv) stayed
~30 ms. Frame time 34 ms -> 19 ms. IFC_CULL_THREADS=0 forces the
single-threaded fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 1ec273f508 ifcviewer: README — document event-driven rendering and VBO quantization
Add the event-driven rendering bullet (zero idle cost, in-render frame
timing) and roadmap entries for VBO quantization and event-driven
rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 574bcfa6c5 ifcviewer: quantize VBO to 16 B/vertex (sidecar v6)
Position now u16x3 normalized against each mesh's local AABB; normal
oct-encoded to i16x2; RGBA8 colour unchanged. Per-mesh dequant basis
lives in a new MeshGpu SSBO at binding 2; both main and pick shaders
mix() against it before applying the instance transform.

Drops VBO and sidecar size by ~43 % (28 -> 16 B/vert), which matters
mostly for warm-load downloads of precomputed sidecars and steady-state
VRAM. LodBuilder dequantizes positions into a scratch buffer before
calling meshopt, since meshoptimizer needs float positions.

Also fixes a streaming-time crash in cullAndUploadVisible: bvh_items
was only populated at finalize, but the linear fallback indexes it
during streaming. Mirror BvhItem appends in uploadInstanceChunk so the
hot path stays valid before the BVH is built.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 09ffdd2028 ifcviewer: event-driven rendering, idle scenes cost zero CPU
Replaced the 16ms QTimer with QEvent::UpdateRequest delivered via
requestUpdate(), posted from every state mutator (mouse/wheel, model
lifecycle, selection, visibility, resize).  A static BIM scene — the
common case for a viewer — now does no work at all between user actions.

FPS is now measured as time spent inside render() rather than wall-clock
gap between frames, so idle gaps don't pollute the 1-second window and
the headline number reflects real render throughput.  Headline fps still
caps at vsync; sub-vsync profiling lives in the cull[...] phase timers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult d0c5bd5e85 Cull: skip cullAndUploadVisible + HiZ on still frames
render() was re-running the full cull every 16 ms timer tick even when
nothing had changed — the camera matrices, scene state, and therefore
visible set were all identical to the previous frame's.  The GPU was
still happy to redraw from the cached indirect buffer, but the CPU was
burning 21 ms/frame rebuilding the same visible list.

Detect the no-op case by comparing view/proj against last_cull_view_ /
last_cull_proj_ and checking a scene-dirty flag (have_cached_cull_)
that every mutator on models_gpu_ invalidates — finalizeModel,
applyCachedModel, applyLodExtension, hide/show/remove/reset, and
uploadInstanceChunk.  When the check passes we skip both
cullAndUploadVisible and buildHizPyramid (the depth buffer is
bit-identical, so re-reading it produces the same pyramid).

Per-model visible_objects / visible_triangles stats now live on
ModelGpuData so the stats line reports correct numbers on skipped
frames instead of reading from a stale indirect_scratch_.

Measured on a 569k-object overview: still frames go 22 fps → 62 fps;
orbiting goes 23 fps → ~30-50 fps depending on how hard you move the
mouse (the cull only pays its full cost on the ~25 % of frames where
the camera actually moved).  The stats line gains a "skipped N/M"
field so you can see the ratio live.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult c03a7fe117 Cull: read AABBs from compact bvh_items in the hot path
cullAndUploadVisible was reading each instance's AABB through
m.instances[idx] — a 104-byte InstanceCpu struct — for the frustum /
contribution / HiZ tests.  Only 24 of those bytes (the two float[3]
AABBs) are actually used by the tests; the rest (4×4 transform +
header) is pure cache-line waste, and with 569k instances the array
is 59 MB, well past any cache.

bvh_items[idx] already stores a 1:1 compact 28-byte record with the
same AABB, built unconditionally in buildBvhForModel().  Switch the
hot test path to read from it, and only touch InstanceCpu once an
instance has passed all three tests (for mesh_id).  Modest ~20 %
drop in cull-traverse time on a 569k-object overview (26 ms → 21 ms).

Also add four cull-phase timers (clr / trv / emt / upl) to the
per-second stats line so future optimisation work has concrete
numbers to chase.  Confirmed via these timers that bucket clears,
emit and GPU upload are all <1 ms combined; traversal is where the
remaining CPU cost lives.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 8596d53a4a Phase 3C: Hierarchical-Z occlusion culling (CPU-side v1)
After the main draw, blit the MSAA default-framebuffer depth to a
single-sample 256×128 depth texture, read it back, and build a CPU
max-reduced mip pyramid.  Next frame's cullAndUploadVisible projects
each BVH node / instance AABB through the previous frame's VP and
compares the AABB's nearest depth against the pyramid's deepest value
at the matching mip level; strictly-beyond AABBs are rejected.

Conservative direction (aabb_near > hiz_max) — never wrongly rejects a
visible instance, so no flicker.  BVH subtree-level test lets a single
8-corner projection reject up to a leaf's worth of instances.

Tuning knobs: IFC_NO_HIZ=1 disables; IFC_HIZ_SIZE overrides base width.
New stats counter hiz_rej shows rejects/frame.

Measured: big win on interior views (GPU-bound), roughly zero net
effect on exterior overviews (CPU-bound on cull traversal, so the
saved GPU work is masked).  Tried a 3-deep PBO ring for async readback
and reverted — the extra frame of staleness produced visible flicker
on fast orbit, and the synchronous readback wasn't actually a measured
bottleneck at 256×128.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult c78e16eafb Phase 3B: per-instance LOD via meshoptimizer simplifySloppy
Decimate each unique mesh once at sidecar-build time and swap to the
reduced index slice per-instance per-frame when projected sphere radius
drops below IFC_LOD1_PX (default 30).  Same VBO, same SSBO, just a
different firstIndex/count in the indirect command.

Extends MeshInfo (48→56 B) with lod1_ebo_byte_offset + lod1_index_count
and bumps the sidecar to v5.  buildLods() runs inside
onStreamingFinished, appends decimated indices to sd.indices,
applyLodExtension pushes the EBO suffix to the live GPU state, and the
sidecar is written with LOD1 baked in.

simplifySloppy (voxel clustering) is used instead of the default
edge-collapse meshopt_simplify because BIM brep output is per-triangle-
unwelded and non-manifold after welding — simplify returned the input
unchanged for every mesh tested.  Sloppy ignores topology.  Knobs
(IFC_LOD_SLOPPY, IFC_LOD_ERROR, IFC_LOD_RATIO, IFC_LOD_MIN_SAVINGS,
IFC_LOD_LOCK_BORDER, IFC_LOD_DEBUG) are available for A/B tuning.

Result on the 128M-tri 10-model test scene (GTX 1650, 2px contribution
cull): 20.2 → 43.2 fps, 40M → 14M visible triangles, no change in
object count.  LOD build adds 100–600 ms per model on first open,
cached thereafter.

README Phase 3B section is now a full writeup of pipeline, selection,
decimator-choice rationale, env vars, and measured numbers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 68fea7bd45 README: mark Phase 3A done with measured numbers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 90366f8236 Phase 3A: screen-space contribution culling
Reject frustum-visible objects whose bounding sphere projects below a
pixel-radius threshold.  Applied at both BVH-node level (whole subtrees
pruned) and per-instance level; short-circuits when the camera is
inside the AABB so nothing-you're-standing-next-to is ever lost.
Pick pass passes threshold 0 so sub-pixel objects stay clickable.

Threshold defaults to 2 px (radius), overridable via IFC_MIN_PX env
var.  Measured on the 128 M-tri test scene (GTX 1650):

  0 px (off):   6.7 fps, 128 M tris
  2 px:        20.2 fps,  40 M tris (31%)
  4 px:        30.3 fps,  15 M tris (12%)

The metric is sphere-based (cheap: one sqrt per test) rather than
AABB-corner projection; loses a little precision on very elongated
bounds but costs ~5x less per test and the BVH-node pre-cull means
the long-tail-of-small-things case is already handled by subtree
pruning before we touch individual instances.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult d3c21d7a81 Pivot Phase 3: diagnose as draw-bound, not upload-bound
Earlier probes pointed at per-frame glNamedBufferSubData uploads as the
bottleneck (60 fps when those two calls were commented out).  That was a
false reading — zeroing the uploads also emptied the indirect buffer, so
MDI drew nothing.  "No upload" and "no draw" were indistinguishable.

Two new diagnostic env vars in render() isolate the real costs:

  IFC_SKIP_MDI=1       keep cull + upload + binds, skip only the MDI
                       draws.  Gives 62 fps with everything else running,
                       confirming the non-draw path fits in ~16 ms.
  IFC_MAX_SUBDRAWS=N   cap each MDI's drawcount.  67k -> 30k sub-draws
                       saves 0 ms, confirming sub-draw count itself is
                       not the bottleneck; the long tail of sub-draws
                       carries ~no triangles.

On a GTX 1650 with 128 M triangles in view, nvidia-smi sits at 95 %
GPU util and FPS scales with triangle work, not sub-draw count.  The
card is simply rasterising at ~850 M tri/s.  No CPU-side or upload
trick recovers it.

Revised Phase 3 is therefore shedding triangles, not bytes:
  3A screen-space contribution culling (next)
  3B LOD
  3C HiZ occlusion
  3D GPU-side compute culling

README Phase 3 section rewritten around the diagnosis, including the
false lead, so future work doesn't re-tread the upload path.  The
aborted staging+resident ring-buffer implementation was reverted (the
uncommitted working tree is gone — pure glNamedBufferSubData retained
for the visible + indirect buffers, which we now know is fine).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult cd77c557e9 Rewrite README for instancing pipeline and refocus Phase 3
The previous README described a pre-instancing world (32-byte world-
coord vertices with per-vertex object_id, ObjectDrawInfo structs, EBO
reordering after BVH build, and a Phase 3 plan built around moving
draw submission to the GPU).  Most of that is either gone or already
solved:

  - Vertices are now 28 B local-coord; per-instance transforms live
    in an SSBO read through a visible-index SSBO and gl_BaseInstanceARB.
  - ObjectDrawInfo is replaced by MeshInfo + InstanceCpu + InstanceGpu.
  - No EBO reorder on BVH build — the BVH is over instance AABBs and
    the mesh/EBO layout is orthogonal.
  - Draw-call submission is already one glMultiDrawElementsIndirect
    per model; the old Phase 3 goal is met.

New content worth keeping:

  - GPU instancing section documents the mesh/instance/visible/indirect
    buffer contract the whole renderer hangs off of.
  - Reflection-aware two-pass draw is documented (det<0 placements,
    forward/reverse slice split, glFrontFace toggle).
  - reorient-shells and backface culling are called out as correctness
    + perf levers with their tradeoffs.
  - Phase 3 is rewritten around the actual bottleneck surfaced by
    profiling: per-frame glNamedBufferSubData stalls on the visible
    and indirect buffers.  Includes the diagnostic methodology (empty-
    screen jump to 60 fps, window/MSAA invariance, upload-comment-out
    experiment) so future-me remembers why this is the next step.
  - 3A (persistent mapped ring buffers, near-term) and 3B (GPU-side
    compute cull, longer-term) split out with scope estimates.
  - Roadmap updated: instancing / MDI / reflections / reorient-shells
    / backface cull all ticked; 3A surfaced as the next open item.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 3110c98429 Backface culling with reflection-aware two-pass MDI
Enables GL_CULL_FACE by default (user-toggleable in Settings) so
closed solids skip shading their back halves.  The catch is that
IFC placements can contain reflections (mat4 with det<0 — mirrored
families, symmetric instances).  Naively culling would make every
mirrored instance vanish because the rasterizer sees its screen-space
winding as backwards.

Fix: detect reflections at upload time via determinant sign, bucket
visible instances into forward (det>=0) and reverse (det<0) per mesh
during culling, and issue two glMultiDrawElementsIndirect calls per
model with glFrontFace toggled CCW/CW between them.  The indirect
buffer is still one buffer — just split into a forward slice followed
by a reverse slice, with m.indirect_forward_count recording the split.

Vertex shader flips the normal when the transform has negative
determinant, keeping lighting correct on mirrored instances.  The
fragment shader keeps the gl_FrontFacing fallback as a safety net
when culling is disabled (e.g. for files with open shells).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 0e2a62d3b7 Enable reorient-shells in geometry iterator
IFC files routinely have IfcConnectedFaceSets whose faces point
inconsistently within the same shell — the result under per-vertex
normals is dark inside-out patches, and under GL_CULL_FACE it's
swiss-cheese.  reorient-shells fixes the face winding at geometry
generation time, which is the only place it can be fixed correctly;
no shader trick can recover from a mesh whose triangles disagree
among themselves.

Off by default in IfcOpenShell because it adds iterator time, but
we cache the result in the sidecar so it's a one-shot cost per file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult f532624dc1 Two-sided lighting, rename misleading draw-count stat
Two bugs conflated as "weird colors":

1. Two-sided lighting.  IFC placements often embed reflection
   matrices (mirrored families).  Transforming a_normal by
   mat3(inst.transform) produces a normal pointing the wrong way
   on those instances, and max(n·L, 0) then clamps the surface to
   pure ambient — reads as dark / washed out.  Use gl_FrontFacing
   to flip n in the fragment shader so both winding orientations
   shade correctly.  The proper fix (ship an inverse-transpose
   normal matrix or a det-sign bit per instance) is still owed;
   that would unlock re-enabling GL_CULL_FACE for a big fragment-
   work win on closed solids.

2. Stats label "inst_draws" was counting indirect sub-draws, not
   actual GL draw calls — misleading since MDI collapses N sub-
   draws into one glMultiDrawElementsIndirect.  Split into
   gl_draw_calls (real GL calls, = drawn-model count) and
   indirect_sub_draws (packed sub-commands).  For a BIM model
   with 47k unique meshes at full view this now correctly reads
   "1 gl_draws (47092 sub)" rather than suggesting 47k driver
   dispatches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult f0e3056d0a Collapse per-mesh draws into glMultiDrawElementsIndirect
Each visible model now issues a single glMultiDrawElementsIndirect
call instead of one glDrawElementsInstancedBaseVertex per mesh.  The
CPU BVH cull populates an array of DrawElementsIndirectCommand
records plus the flat visible-instance list, uploads both, and draws
the whole model in one GL call.

Vertex shaders switch from a uniform u_instance_offset to
gl_BaseInstanceARB (ARB_shader_draw_parameters), so per-draw offset
comes from the indirect command's baseInstance field.

Draw-call counts for BIM scenes with hundreds of unique meshes drop
from hundreds-per-frame to one-per-model, cutting driver overhead.
This also sets up the plumbing for the follow-up compute-shader cull
that will populate the indirect buffer entirely on-GPU.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 298eca0ab6 Progressive rendering during streaming
Pre-allocate the instance SSBO on model creation (4 MB, grow-on-demand)
and append each arriving InstanceChunk directly to the GPU-side
InstanceGpu array in uploadInstanceChunk.  This makes a model drawable
as soon as its first mesh + first instance chunk land, rather than
waiting for finalizeModel.

The visible-list architecture already decouples SSBO order from the
draw path, so appending in insertion order is correct — no sorting
required.  finalizeModel collapses to:
  - compute per-mesh instance counts (for stats + sidecar round-trip)
  - build the per-model BVH over instance world AABBs

Render / pick loops now gate on ssbo_instance_count > 0 rather than
the finalized flag.  Stats include in-progress models in totals
(excluding only hidden).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 1f17d73f3e BVH frustum culling over instances
Re-wires the BVH acceleration structure on top of the new instanced
renderer.  Per model, build a BVH over per-instance world AABBs at
finalize (and on sidecar apply).  Each frame, traverse the BVH against
the camera frustum to produce a visible-instance index list, bucket by
mesh_id, and upload to a per-model SSBO at binding=1.  The main and
pick vertex shaders do a double-indirection
`instances[visible[u_offset + gl_InstanceID]]` so draws only touch
instances that passed the frustum test.

Models with fewer than BVH_MIN_OBJECTS instances skip the BVH build
and fall back to a linear per-instance frustum test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult ababb49ae7 Sidecar v4: persist instanced geometry + metadata
Commit B of the instancing migration.  The sidecar on-disk format is
reintroduced at version 4 with MeshInfo + InstanceCpu sections in place
of v3's flat per-object draw-info array.

After streaming finishes, MainWindow asks the viewport for a post-
finalise snapshot (VBO + EBO are read back from the GPU, meshes and
instances come from the CPU-side arrays) and writes it alongside
PackedElementInfo + the string table.  On a subsequent load,
readSidecar rehydrates the whole struct and ViewportWindow::
applyCachedModel uploads VBO/EBO/SSBO in a single step, bypassing the
iterator entirely.

Staleness check is still by source file size.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 07a5c59359 GPU instancing: streamer, viewport, shaders rewritten
Commit A of the instancing migration (Phase 3a).  The streamer now runs
the iterator with use-world-coords=false and dedupes by the geometry's
representation id, emitting a MeshChunk once per unique geometry and an
InstanceChunk per placement.  The viewport keeps geometry in local
coordinates (28 B/vertex, down from 32) and applies the per-instance
transform in the vertex shader via an std430 SSBO indexed by
gl_InstanceID + a per-draw uniform offset.  After streaming finishes
finalizeModel() stable-sorts instances by mesh_id, assigns each mesh a
contiguous range, and uploads the SSBO; render then issues one
glDrawElementsInstancedBaseVertex per mesh.

BvhAccel is reshaped to operate on a generic BvhItem (world AABB +
model_id) so it can drive instance-level culling, but the path is not
wired in yet -- every instance is drawn every frame in this commit.
Progressive-during-streaming rendering is likewise disabled: a model
appears when its SSBO is uploaded, not incrementally.  Sidecar cache
is stubbed (reads miss, writes are no-ops); the v4 on-disk format with
MeshInfo + InstanceGpu sections lands in Commit B.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 8c8ef5c32b Leaf-batched BVH draw commands
When a BVH leaf passes the frustum test, emit a single glMultiDrawElements
record covering the leaf's entire index range instead of one per object.
Leaves are contiguous in the EBO after reorderEbo, so the range is just
[first_object.index_offset, sum(index_count)]. Cuts draw calls by ~8x
(BVH_MAX_LEAF_SIZE) and shifts the bottleneck from CPU/driver per-draw
overhead toward GPU vertex throughput.

Per-object features (selection highlight, per-vertex color, object_id
picking) are unchanged — they operate on vertex attributes, not draw
state. Future per-object hide/override will use SSBO lookups sampled
by object_id in the fragment shader.

Slight overdraw from skipping per-object frustum tests within a leaf is
negligible given median-split BVH tightness and spare tri throughput.

Also adds visible_objects_ counter so stats still report true object
counts (not leaf counts), plus leaf_draws/model_draws breakdown in the
per-second frame log.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 36fa53122a Add profiling for VRAM, FPS ratios, and instancing analysis
Per-second frame log reports fps/ms, visible/total object & triangle
ratios, VRAM breakdown (VBO+EBO), model count, and pending uploads.

Upload-complete log includes per-model VBO/EBO MB and scene total VRAM.

Streamer runs an instancing analysis keyed on geom.id(): total shapes,
unique representations, dedup ratio, theoretical VBO/EBO/SSBO sizes if
instanced, potential savings, and top-5 most-duplicated representations.
Used to validate whether GPU instancing is worth the architectural
rewrite for a given dataset.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 1dace18d26 BVH frustum culling, sidecar cache, per-model buffers, progressive upload
Phase 2 performance: BVH acceleration with median-split build, per-model
trees, and EBO re-sorting for GPU cache coherence. Raw binary .ifcview
sidecar stores full geometry + BVH for instant subsequent loads (skip
tessellation entirely).

Per-model GPU buffers (VAO/VBO/EBO per model) eliminate cross-model buffer
copies on growth. Sidecar reads happen on a background thread. Bulk GPU
uploads are progressive (48 MB/frame chunks) so the viewport stays
interactive while multi-GB models stream in.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 5b4c1089cf Update README for multi-model support and frustum culling
Reflect current architecture: per-model streamers, glMultiDrawElements
with frustum culling, 32-byte vertex format with color, multiselect
file picker, settings/stats files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 83a3131276 Multi-model project support with sequential loading
Introduce ModelHandle and per-model GeometryStreamers so multiple IFC
files can be loaded simultaneously. Object IDs are globally unique
(monotonically increasing across models). File picker is now multiselect.
Each model gets a top-level tree node. Property lookup uses the correct
model's ifcopenshell::file. ViewportWindow supports hide/show/remove
per model via model_id filtering in the frustum cull pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 6f6bebf387 Add performance stats overlay in status bar
Show FPS, frame time, visible/total objects, and visible/total
triangles in the status bar. Toggled via Settings > Show Performance
Stats, persisted in app settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult bfac12dbe7 Per-object frustum culling with glMultiDrawElements
Track per-object AABB and index range during upload. Each frame,
extract frustum planes from the view-projection matrix and cull
objects whose AABB is entirely outside any plane. Draw only visible
objects via glMultiDrawElements. Document the three-phase rendering
performance strategy in README.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult d33055bb72 Plan out performance strategy 2026-04-23 21:32:24 +10:00
Dion Moult d08a4e0706 Update ifcviewer to compile with datamodel refactor 2026-04-23 21:32:24 +10:00
Dion Moult 06eca938d7 Dump of hello world ifc viewer code 2026-04-23 21:32:24 +10:00
Dion Moult 543d9f8588 Local hacks to compile and monkey patch issues in the Python world
All AI generated slop. Do NOT trust these "fixes". It's just to get it
working on my machine.
2026-04-23 21:32:24 +10:00
Thomas Krijnen b40e4378b3 update workflows 2026-04-23 10:58:57 +02:00
Thomas Krijnen 1089de14f0 quotes 2026-04-23 10:51:52 +02:00
Thomas Krijnen 1b35533917 Update workflow for .so copying 2026-04-22 21:40:05 +02:00
Thomas Krijnen c42a7f32d0 The proper id / identity fix for rocksdb 2026-04-22 18:11:01 +02:00
Thomas Krijnen 14e9846e35 identity_ for types; id_ for instances 2026-04-22 12:04:24 +02:00
Thomas Krijnen 37c6aea092 forgot to set goosd 2026-04-22 12:04:09 +02:00
Thomas Krijnen 9b13dc8dd6 Get rid of parse context pool 2026-04-22 12:03:58 +02:00
Bruno Postle e4f5c630db Add license for OpenGost font shipped with Bonsai
Extracted from the font file like so:
python3 -c "
  from fontTools.ttLib import TTFont
  tt = TTFont('src/bonsai/bonsai/bim/data/fonts/OpenGost Type B TT.ttf')
  for record in tt['name'].names:
      if record.nameID == 13:
          print(record.toUnicode())
  "
2026-04-21 23:44:14 +01:00
Thomas Krijnen 89c66f62bf Python import fixes: import from wrapper now which inherits from mixins 2026-04-21 21:59:02 +02:00
Thomas Krijnen 18363c0d19 No soname for plugin.so in CREATE_BUNDLE mode 2026-04-21 21:52:04 +02:00
Thomas Krijnen 782aa4f88f Copy more so to python module 2026-04-21 21:51:21 +02:00
Thomas Krijnen 13c71d7a8a symbol visibility 2026-04-21 21:48:54 +02:00
Thomas Krijnen cb7e7331e6 CREATE_BUNDLE=On to copy .so 2026-04-21 21:48:15 +02:00
Thomas Krijnen 4850e2a07c add manifold to build-all.py 2026-04-21 21:47:38 +02:00
Thomas Krijnen fd980abcc2 Reverse subdir order so that svgfill is a proper target 2026-04-21 21:46:56 +02:00
Thomas Krijnen b022ca7e70 Some plug-in work 2026-04-21 16:18:59 +02:00
Thomas Krijnen 325db2e57f Export templates 2026-04-21 11:55:04 +02:00
Massimo Fabbro 4adaf0d61f See #6853. Minor fix for IfcDoor with IFC4x3 quantity calculation with blender engine 2026-04-20 17:55:49 +02:00
Massimo Fabbro e392d2da6e See #7716. Remove_cost_item also delete the assignment
Previously remove_cost_item leaved orphaned relation now it should be fixed
2026-04-20 17:17:23 +02:00
Massimo Fabbro 5febbc1391 See #7716. Fix util get_cost_item_for_product
Before there was an error if there weren't assignments now it should be fixed. Add also tests.
2026-04-20 17:17:23 +02:00
Massimo Fabbro 6b2d25a5e5 Add tests for cost tool 2026-04-20 17:16:08 +02:00
Massimo Fabbro 2d05398b1c fix infinite recursion error
previously there was an almost silent error because the update function was called every time. Now it should be fixed.
2026-04-20 17:16:08 +02:00
Thomas Krijnen 046ceb452a Tighten scope of cmake vars and dirs 2026-04-19 12:32:18 +02:00
Thomas Krijnen 9e19735275 IfcConvert Plug-in discovery for info print 2026-04-19 12:32:10 +02:00
Thomas Krijnen 6ee05b646b swig ignore Base::Base(std::nullopt_t); 2026-04-19 12:24:36 +02:00
Thomas Krijnen e62921171c Remove duplicated attr in wrapper 2026-04-19 11:42:24 +02:00
Thomas Krijnen 6c47123781 Remove C++ references to ifcxml 2026-04-19 10:35:04 +02:00
Thomas Krijnen d448fa95e3 build-all.py use single build dir 2026-04-19 10:21:23 +02:00
Thomas Krijnen 57de09d236 Pointer issue 2026-04-19 09:31:49 +02:00
Thomas Krijnen a25f522cc3 typo 2026-04-19 08:33:48 +02:00
Thomas Krijnen e067c2b834 build-all BUILD_SHARED_LIBS tweak 2026-04-18 21:43:36 +02:00
Thomas Krijnen e2905d6f0e BUILD_SHARED_LIBS=On 2026-04-18 21:32:32 +02:00
Thomas Krijnen ead061a98a svgfill dll link related 2026-04-18 21:13:24 +02:00
Thomas Krijnen fae85e63cc std::optional 2026-04-18 21:06:39 +02:00
Thomas Krijnen bccea6d932 Examples and update virtual bases for new codegen 2026-04-18 21:04:42 +02:00
Thomas Krijnen 1cc93784cd Rerun codegen 2026-04-18 21:03:02 +02:00
Thomas Krijnen f1e93581ec Reuse constructors and return *this from initialize() 2026-04-18 21:01:21 +02:00
Thomas Krijnen 91ae631c7d Merge remote-tracking branch 'origin/v0.8.0' into datamodel-v1.0 2026-04-18 20:15:28 +02:00
Thomas Krijnen b599ee1040 More work on isolating into plug-ins 2026-04-18 15:46:21 +02:00
Thomas Krijnen d2cc66fdf0 tree and document plug-ins 2026-04-17 11:24:09 +02:00
Thomas Krijnen 32a7de66de ifcchat: update ifopsh to latest wasm wheel 2026-04-17 10:05:25 +02:00
Thomas Krijnen 3824e7b449 First start plug-in architecture 2026-04-15 18:07:28 +02:00
Andrej730 29fe41edd0 maintenance: rename main.yml to publish-websites.yml in docs 2026-04-15 16:08:21 +05:00
Andrej730 760c65595c build_rocky: use uv to acquire more recent version of Python 2026-04-15 14:32:45 +05:00
Andrej730 29b648d8dd Makefiles - refer to python in more generic way 2026-04-15 11:26:11 +05:00
Andrej730 3ffdb9e74d maintenance: add publish-bonsai-releases.py to Blender Python version update checklist 2026-04-15 10:52:43 +05:00
Andrej730 00915409ac maintenance: add documentation about multiple Blender Python versions 2026-04-15 10:50:44 +05:00
Andrej730 d21543a24a maintenance: add corrective release documentation 2026-04-15 10:46:12 +05:00
Andrej730 9246be710c black . 2026-04-14 20:01:21 +05:00
Andrej730 3205a4ebb1 Add workflow to publish bonsai releases to Blender Extensions 2026-04-14 20:01:21 +05:00
dependabot[bot] e82c087b5e Bump ruff from 0.15.9 to 0.15.10
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.9 to 0.15.10.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.9...0.15.10)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 19:43:35 +05:00
Thomas Krijnen aa10784154 First slice plug-in refactor 2026-04-14 13:51:09 +02:00
Thomas Krijnen 2018bcb3c1 This needs some serious scrutiny 2026-04-13 21:28:55 +02:00
Thomas Krijnen 11006f0ef5 deque 2026-04-13 21:18:58 +02:00
Thomas Krijnen a44f72287b pasta errors 2026-04-13 21:18:49 +02:00
Thomas Krijnen c6849073d9 Reset weights 2026-04-13 21:18:41 +02:00
Andrej730 e6258ab4a8 Bump VERSION to 0.8.6
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 19:40:25 +05:00
Andrej730 5db65f4041 maintenance - list all things we do on release 2026-04-13 19:39:14 +05:00
Andrej730 89ce32fdfd Remove redundant docs-deployment.yml workflow
The https://github.com/IfcOpenShell/website repo already has bonsai-docs.yml workflow that does the same thing - builds Bonsai docs from the main repo and deploys to bonsaibim_org_docs, so this workflow is redundant and confusing.
2026-04-13 19:39:14 +05:00
Andrej730 763a31a31d readme: fix ifcsverchok badge filter 2026-04-13 19:38:25 +05:00
Andrej730 4b8c612647 fix ifcmcp package name inconsistency 2026-04-13 18:44:01 +05:00
Andrej730 16723d11ca ci-pyodide-wasm-release - add tag when pushing release 2026-04-13 17:45:37 +05:00
Andrej730 67238c4ac1 ci-pyodide-wasm-release - use BUILD_REPO_TOKEN 2026-04-13 17:40:02 +05:00
Andrej730 20229aa88c README.md: add pyodide-wasm-wheels tag badge 2026-04-13 16:43:20 +05:00
Andrej730 7788ae86c9 build-all.py: descriptive error for missing SSL support 2026-04-13 16:23:41 +05:00
Bruno Postle 002b7c5d6e ifcquery, ifcmcp: better bot selector syntax hints 2026-04-10 22:09:56 +01:00
Thomas Krijnen e7db239647 inverse access in schema 2026-04-10 21:46:39 +02:00
Thomas Krijnen 158756e921 arrange_polygons: settings, simplify based on growing boxes; more... 2026-04-10 21:46:39 +02:00
Andrej730 a3efa7e9ee util.element - fix IfcComplexProperty KeyError when verbose=True (#7921)
Introduced by me in b77df1892
2026-04-10 19:11:42 +05:00
Andrej730 4896946e78 ty ignore some upstream bpy stubs issues 2026-04-10 19:11:41 +05:00
Andrej730 588f365366 Remove unused ty ignores - issue is resolved upsteam in stubs 2026-04-10 19:11:41 +05:00
Andrej730 fa8770c14d ty - drop rules removed from recent version of ty 2026-04-10 19:11:41 +05:00
Andrej730 0cf831133e ci-lint - add ty type check 2026-04-10 19:11:41 +05:00
Andrej730 98338e0831 Rename ci-black-formatting workflow to ci-lint 2026-04-10 18:06:43 +05:00
Andrej730 5a3160eb62 black . 2026-04-10 18:02:47 +05:00
Andrej730 80a9df8f52 Ignore pyright warnings for bpy stubs
See https://github.com/nutti/fake-bpy-module/discussions/440
2026-04-10 18:01:19 +05:00
Andrej730 8eb0060d4a Get rid of pyright ignore reportRedeclaration noise
Welp, it was helping to point out untyped props, but it is getting too noisy now.
2026-04-10 17:55:16 +05:00
Thomas Krijnen b2fc0c00cc Hierarchical index for inverses 2026-04-10 14:54:47 +02:00
falken10vdl 51a338e4c8 Suppress reportRedeclaration in Pyright config 2026-04-10 17:49:14 +05:00
Thomas Krijnen 2d5883f966 Dilation of 2nd operands as a poor mans fuzziness 2026-04-10 14:46:46 +02:00
Andrej730 7169dcd053 Create ci-pyodide-wasm-release.yml 2026-04-10 17:29:46 +05:00
Andrej730 b9d4ea38b0 Script for packing pyodide wheel 2026-04-10 17:29:46 +05:00
Andrej730 c8f46cfb69 build_pyodide.sh - use emsdk from pyodide 2026-04-10 16:22:04 +05:00
Andrej730 6242251d3c Fix typo 2026-04-10 16:22:04 +05:00
Andrej730 1689960257 Maintenence - document ci-bonsai.yml update 2026-04-10 16:20:50 +05:00
dependabot[bot] c509f1d3ee Bump vite from 6.4.1 to 6.4.2 in /src/ifctester/webapp
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.4.1 to 6.4.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.4.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.4.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 16:16:33 +05:00
Dion Moult 217bfed847 Add py313 to stable build 2026-04-10 19:05:54 +10:00
Thomas Krijnen 4621fc9269 Less useless logging 2026-04-10 09:54:28 +02:00
Thomas Krijnen 1840e3d1a8 Add passthrough kernel 2026-04-09 17:17:53 +02:00
Thomas Krijnen becd38c77d Compilation fixes 2026-04-09 16:18:53 +02:00
Thomas Krijnen 7d6c6bd523 Fix iteration: prevent inserting nullptr equivalents into a set 2026-04-09 11:53:28 +02:00
Bruno Postle b4558f7f75 Fix ruff import ordering complaints 2026-04-09 01:04:14 +01:00
dependabot[bot] ebd5fe854f Bump ruff from 0.15.8 to 0.15.9
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.8 to 0.15.9.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.8...0.15.9)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-09 09:59:16 +10:00
dependabot[bot] 06cfd0931c Bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-09 09:59:10 +10:00
dependabot[bot] 90bd7d26ac Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-09 09:59:03 +10:00
Thomas Krijnen 3fbf01f446 partial revert of 24acfea 2026-04-08 13:48:23 +02:00
Bruno Postle 26a1955cba Fix compilation failure introduced in 24acfea 2026-04-07 23:34:30 +01:00
Bruno Postle 27d9cae8ff Bonsai, bump ifcmerge.exe to working version with deps
Don't leave a broken repo if ifcmerge is misinstalled.
Fix bug where only local branches could be merged.
Fix gitch where merge commits were not considered relevant.
2026-04-07 22:25:31 +01:00
Thomas Krijnen a425bb6da0 Cache some hotpath inverses regarding context handling 2026-04-07 16:13:01 +02:00
Thomas Krijnen a19d398c78 Vibe code an implementation that uses manifold 2026-04-07 15:47:58 +02:00
Thomas Krijnen a751c1cce3 ifcchat: compaction 2026-04-07 09:46:45 +02:00
Thomas Krijnen 9d4307d343 ifcchat: Throttling of messages based on estimated token counts 2026-04-07 09:46:11 +02:00
Ryan Schultz ab7d9fdf4a Auto-assign aggregate on eyedropper pick
Add update callbacks to the relating_object and related_object
PointerProperties so that selecting an object via the eyedropper
in BIM_PT_aggregate immediately calls aggregate_assign_object
and closes the editing panel, removing the need to click the
checkmark button manually.

Generated with the assistance of an AI coding tool.
2026-04-05 16:43:03 -05:00
Ryan Schultz 5436467fc5 Whoops, this was supposed to be a PR...
Revert "Fix #3742: Remove coplanar boundary lines between adjacent same-material elements in Bonsai SVG drawings"

This reverts commit 1c7e134d78.
2026-04-04 13:49:02 -05:00
Ryan Schultz 1c7e134d78 Fix #3742: Remove coplanar boundary lines between adjacent same-material elements in Bonsai SVG drawings
Adds `remove_coplanar_boundary_lines()` to operator.py (Bonsai uses this
path, not draw.py's main()). After `merge_linework_and_add_metadata()`
assigns material CSS classes, this post-processes the SVG to delete
projection line segments that appear in two or more adjacent, coplanar
elements with the same material and presentation style.

Key design decisions:
- Material identity: compared via sorted IFC material ID tuples from
  `get_materials()`, not CSS class names — avoids false matches between
  unrelated `material-null` elements.
- Presentation style identity: compared via IFC IfcPresentationStyle IDs
  from `StyledByItem` on geometry representation items — handles elements
  with no material but distinct visual styles.
- Physical adjacency: confirmed by a 3D shared-vertex test (tol=0.01 m)
  after a quick AABB guard, rejecting elements whose 2D projections
  overlap but sit at different depths.
- Coplanarity: determined by the dominant (largest-area) face normal of
  each Blender mesh object — area-weighted averages are unreliable for
  slabs whose equal top/bottom faces cancel out. Folded walls sharing an
  edge but meeting at an angle are correctly rejected (normal dot ≪ 1.0).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 13:42:49 -05:00
Bruno Perdigão 97ee4eaef0 See #7888 - Fix snap when object changes during modal operator.
Handle cases where the snapped target is modified while a modal operator
is active (e.g., adding a door or window that alters the wall geometry).
2026-04-04 14:30:59 -03:00
Thomas Krijnen 30517770e0 Revert default tool output truncation 2026-04-04 13:12:47 +02:00
DesertSpringsCivil 5b1ec85f75 feat: Reduce token usage in ifcchat and default to IFC4X3
- Add Anthropic prompt caching (cache_control on system prompt and
  tools) to reduce repeated token costs by ~90%
- Truncate large tool results in conversation history (2000 char cap)
  to prevent context bloat from ifc_tree/ifc_select responses
- Add sliding window (40 messages) on conversation history, trimming
  at user message boundaries to avoid breaking tool-call sequences
- Default "New IFC" button to IFC4X3 schema instead of IFC4
- Constrain ifc_new schema parameter with enum to prevent invalid
  schema strings like "IFC4X3ADD2"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:11:14 +02:00
Stephen Boddy 12123baafe Update the pyver as 3.13 is default in 5.1 now 2026-04-04 04:22:12 +01:00
Stephen Boddy 0a1c54cedc Fix the ci-bonsai-daily blender url 2026-04-04 03:56:08 +01:00
Bruno Postle 12144f76d3 Merge branch 'ifcgit-features' into v0.8.0 2026-04-04 00:10:33 +01:00
Bruno Postle ca6e950496 ifcgit: conflict report panel and dry-run merge preview
Parse ifcmerge JSON output and display a per-conflict breakdown in the
panel when merge fails. Ctrl+click on the Merge button previews
conflicts without committing. Add SelectConflictEntity operator to
select and frame the conflicting object in the 3D viewport.

Generated with the assistance of an AI coding tool.
2026-04-03 13:28:14 +01:00
Thomas Krijnen c478da5257 Remove pro 2026-04-03 11:36:56 +02:00
Thomas Krijnen c28251a1b1 Add CNAME file 2026-04-03 11:30:33 +02:00
Thomas Krijnen 9bc0588d21 Update openai model list 2026-04-03 11:30:23 +02:00
Thomas Krijnen 918cc65a0d Provider selection as tabs 2026-04-03 11:22:43 +02:00
Thomas Krijnen 7350ccd25e Tweak header padding 2026-04-03 11:14:40 +02:00
Thomas Krijnen c1f146966c The end of open source? Just regurgitate some markdown parsing code. 2026-04-03 11:03:44 +02:00
Thomas Krijnen 24acfeaf45 Thinking indicator under chat 2026-04-03 10:59:09 +02:00
Thomas Krijnen 5d748c5b04 Add Gemini option 2026-04-03 10:49:36 +02:00
Thomas Krijnen 5bcf8685ab Merge remote-tracking branch 'origin/feat/ifcchat-claude' into v0.8.0 2026-04-03 10:26:29 +02:00
geronimi73 8f64f75abb add favourite models 2026-04-03 09:46:44 +02:00
geronimi73 434ea74f22 format this mess 2026-04-03 09:46:44 +02:00
geronimi73 fbf2946b69 Update index.html 2026-04-03 09:46:44 +02:00
geronimi73 7af0ec13e5 move model to sidebar 2026-04-03 09:46:44 +02:00
geronimi73 29e5e0fd1a chevrons for tool result expansion 2026-04-03 09:46:44 +02:00
geronimi73 d7de4f8df0 dont freeze UI on error 2026-04-03 09:46:44 +02:00
geronimi73 252bd6f4f6 openai by default 2026-04-03 09:46:44 +02:00
geronimi73 3b28c92414 html too big -> styles into sep. file 2026-04-03 09:46:44 +02:00
geronimi73 fcbec74521 spinner 2026-04-03 09:46:44 +02:00
geronimi73 0f7f960b29 let claude code openrouter compatibility 2026-04-03 09:46:44 +02:00
geronimi73 b7fc5daf82 ui: choose openai/openrouter 2026-04-03 09:46:44 +02:00
geronimi73 1d7a8ca249 separate API calls 2026-04-03 09:46:44 +02:00
Ryan Schultz eaf7950677 Fix TypeError in ray_cast_by_proximity_2d degenerate edge
A degenerate edge (zero-length segment) caused an early `return`
of a tuple instead of continuing the loop, resulting in a
TypeError when snap.py iterated the result and tried to assign
`point["group"]` on a float.

Generated with the assistance of an AI coding tool.
2026-04-02 23:24:35 -03:00
DesertSpringsCivil 95851ff94c feat: Add Anthropic Claude API support to ifcchat
Add a provider selector (OpenAI / Anthropic) to the ifcchat web UI,
allowing users to use their Anthropic API key with Claude models
instead of only OpenAI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:50:27 -06:00
Bruno Postle 3a6881ca17 ifcgit: add rename branch button next to working branch label
See #7577

Generated with the assistance of an AI coding tool.
2026-04-03 00:20:22 +01:00
Bruno Postle c5210a4f82 ifcgit: sync load_project post-import steps with project operator
See #7578
2026-04-03 00:03:34 +01:00
Bruno Postle e3cadab406 ifcgit: add clone widget to new project wizard (#7579) 2026-04-02 23:42:39 +01:00
Bruno Postle d5b551874d ifcgit: pre-fill branch name when switching to a remote branch tip
When a remote branch tip is checked out (resulting in detached HEAD),
the new-branch name field is now pre-filled with the local equivalent
of the remote branch name (generating a unique suffix if that name is
already taken), so the commit button is immediately usable.

See #7580

Generated with the assistance of an AI coding tool.
2026-04-02 23:16:40 +01:00
Bruno Postle be3aef59fa ifcgit: move action buttons below revision list in a labelled row
Generated with the assistance of an AI coding tool.
2026-04-02 22:45:33 +01:00
Bruno Postle bcc631bf5f ifcgit: improve colourise to find products via geometry and property changes
Generated with the assistance of an AI coding tool.
2026-04-02 22:44:56 +01:00
Bruno Postle 6c18ac9605 ifcgit: use --prioritise-local flag for ifcmerge-forward mergetool 2026-04-02 22:42:29 +01:00
Bruno Postle 9d42f4c1ee Update Bonsai to latest ifcmerge (#7581 #3096)
This version has some functional differences:
- Structured JSON error message instead of free text (on STDOUT not STDERR)
- New --prioritise-local flag to control which side wins in merge conflicts (not used by Bonsai yet)
- IfcLocalPlacement conflicts now auto-resolve instead of failing the merge (partial solution to #6885)
- Float values are normalised when comparing entities (workaround for #7696)
2026-04-02 07:54:20 +01:00
Ryan Schultz fdb2947345 Add git branch to system info debug output
Include bonsai_git_branch in get_debug_info(). For dev environments
using the GitPython-based update_commit_data() path, the branch is
read from repo.active_branch.name. For built extensions, a 7777777
placeholder is replaced at build time via the Makefile, matching the
existing pattern for bonsai_commit_hash and bonsai_commit_date.

Generated with the assistance of an AI coding tool.
2026-04-01 19:45:22 -05:00
Bruno Postle 5e784e4175 Refactor ifcgit, fix UI bugs and performance
Move all business logic into bonsai core and tool. Performance fixes to
minimise file IO, various minor bug fixes and tests.

Generated with the assistance of an AI coding tool.
2026-04-02 00:03:03 +01:00
Thomas Krijnen fb81c88a5f initial ai chat src 2026-04-01 15:56:13 +02:00
Thomas Krijnen c014ce2b46 initial ai chat src 2026-04-01 15:52:22 +02:00
Thomas Krijnen ff65719074 initial ai chat src 2026-04-01 15:50:24 +02:00
Thomas Krijnen 3491e4c91b initial ai chat src 2026-04-01 15:46:45 +02:00
Thomas Krijnen 9f3adc9154 initial ai chat src 2026-04-01 15:36:27 +02:00
Thomas Krijnen f6c6203408 initial ai chat src 2026-04-01 15:33:43 +02:00
falken10vdl 39a376df95 intersect_edge_region_border: Change return statements to return None, None for no intersection
In order to fix error of the type:
              |     point, _ = cls.intersect_edge_region_border(
                            |     ^^^^^^^^
                            | TypeError: cannot unpack non-iterable NoneType object

a tuple is expected.
2026-04-01 08:46:40 -03:00
Ryan Schultz 70a4fdbf95 Fix #7878: Fix snapping crash with non-mesh objects
Two bugs introduced in 31b571322:
- SnapObj assumed obj.data is always a Mesh; non-mesh
  objects (empties, lights, etc.) have obj.data = None,
  causing an AttributeError on obj.data.edges.
- view3d_utils was used but never imported.

Generated with the assistance of an AI coding tool.
2026-04-01 08:14:51 -03:00
Thomas Krijnen 0a41d2e016 Rename project from 'ifcmcp' to 'ifcopenshell-mcp' 2026-04-01 11:09:49 +02:00
Thomas Krijnen 0b5eab8549 Enable verbose output for PyPI deployment 2026-04-01 09:24:35 +02:00
Bruno Postle 6f9d54c2af ifcquery, ifcedit: update docs for --format ids and foreach subcommand
Add --format ids to the ifcquery.rst format description and a new
"Scripting with ifcedit" section showing composition examples.  Add
the foreach subcommand to ifcedit.rst with usage examples.
2026-04-01 08:53:09 +02:00
Bruno Postle 9ea302cdf1 ifcquery, ifcedit, ifcmcp: add documentation
Add ifcquery, ifcedit and ifcmcp to the README contents table, the
Sphinx docs toctree and introduction utilities table. Add new .rst
pages for each package documenting subcommands, installation, usage,
and parameter types. Fix plot and render CLI examples in ifcquery
README to use -o/--out-format flags. Update ifcmcp README to use the
installed ifcmcp command rather than python3 -m ifcmcp.

Generated with the assistance of an AI coding tool.
2026-04-01 08:53:09 +02:00
Bruno Postle c057e79f17 ifcquery, ifcedit, ifcmcp: add Makefiles and PyPI publish workflows
These three packages were added to src/ but lacked the Makefile needed
by common.mk to build distribution wheels, and the GitHub Actions
workflow to publish them to PyPI.

Adds make dist / make test / make qa targets and ci-*-pypi.yaml
workflows matching the pattern used by ifcpatch, ifcclash, etc.
2026-04-01 08:53:09 +02:00
Andrej730 da470c5135 Fix missing but used initial_t var 2026-04-01 10:37:23 +05:00
Andrej730 214cd44f8e Fix missing view3d_utils import 2026-04-01 10:37:07 +05:00
Andrej730 4bff2fa554 Fix ruff 2026-04-01 10:37:07 +05:00
Andrej730 9d78df392d black . 2026-04-01 10:37:07 +05:00
Andrej730 86bef0a254 typing 2026-04-01 10:37:06 +05:00
Andrej730 05bf59d360 ci-bonsai-daily - bump Blender version to 5.1 2026-04-01 10:37:06 +05:00
Thomas Krijnen f616c4049c Add Codex-generated wrappergen 2026-03-31 20:51:46 +02:00
Thomas Krijnen 20ccf2b455 Parameter naming 2026-03-31 18:23:13 +02:00
Thomas Krijnen a07f56db6f Restructure and rename 2026-03-31 15:32:36 +02:00
Thomas Krijnen 724cdb446e Move schemas into schemas/ subfolder 2026-03-31 10:02:00 +02:00
Bruno Postle 17eaef778a api.geometry.connect_path: add connection_geometry parameter
IfcRelConnectsPathElements has an optional ConnectionGeometry attribute for
recording the geometric cut-plane between adjacent elements, but there was
no way to set it via the API.

Generated with the assistance of an AI coding tool.
2026-03-30 07:30:38 +01:00
Bruno Postle f46be80193 Add api.structural.assign_product, assign_to_building, and api.geometry.add_topology_representation
assign_product creates IfcRelAssignsToProduct linking a structural member to
a physical building element. assign_to_building creates IfcRelServicesBuildings
linking a structural analysis model to a building. add_topology_representation
creates IfcTopologyRepresentation for structural elements, inferring the
representation type from the item class.

Generated with the assistance of an AI coding tool.
2026-03-30 07:28:01 +01:00
Bruno Postle be05d771a2 api.boundary.edit_attributes: add PhysicalOrVirtualBoundary and InternalOrExternalBoundary params
Both attributes are required by the IFC schema but were not settable via
the API function. Add physical_or_virtual and internal_or_external parameters
with "NOTDEFINED" defaults for backward compatibility. Update Bonsai boundary
panel to expose both fields in the editor.

Generated with the assistance of an AI coding tool.
2026-03-30 07:25:22 +01:00
Bruno Postle c214d255c9 Fix api.boundary.assign_connection_geometry TypeError
TypeError: attribute 'DirectionRatios' for entity 'IFC4.IfcDirection' is
    expecting value of type 'AGGREGATE OF DOUBLE', got 'ndarray'
2026-03-29 22:04:59 +01:00
Bruno Postle 0d8ba71384 Fix typo in api.boundary.assign_connection_geometry 2026-03-29 21:46:29 +01:00
Bruno Postle 1c26ee86c9 ifcquery/ifcedit: enable shell scripting by composing query and edit commands
Add --format ids to ifcquery to output step IDs suitable for piping into
ifcedit parameters. Add ifcedit foreach to apply an operation to every
element in a query result. Extend clash and relations output so --format ids
extracts all involved element IDs, enabling one-liners like clash detection
piped directly into render.

Generated with the assistance of an AI coding tool.
2026-03-29 15:17:22 +01:00
dependabot[bot] 0ed96d32dd Bump picomatch from 4.0.2 to 4.0.4 in /src/ifctester/webapp
Bumps [picomatch](https://github.com/micromatch/picomatch) from 4.0.2 to 4.0.4.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.2...4.0.4)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-28 16:53:31 +11:00
dependabot[bot] f96526195d Bump actions/deploy-pages from 4 to 5
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4 to 5.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/deploy-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-28 16:53:12 +11:00
dependabot[bot] fd29481d65 Bump hendrikmuhs/ccache-action from 1.2.21 to 1.2.22
Bumps [hendrikmuhs/ccache-action](https://github.com/hendrikmuhs/ccache-action) from 1.2.21 to 1.2.22.
- [Release notes](https://github.com/hendrikmuhs/ccache-action/releases)
- [Commits](https://github.com/hendrikmuhs/ccache-action/compare/v1.2.21...v1.2.22)

---
updated-dependencies:
- dependency-name: hendrikmuhs/ccache-action
  dependency-version: 1.2.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-28 16:53:03 +11:00
dependabot[bot] 24b48497f0 Bump actions/configure-pages from 5 to 6
Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 5 to 6.
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-28 16:52:57 +11:00
dependabot[bot] 2b9822f141 Bump mamba-org/setup-micromamba from 2 to 3
Bumps [mamba-org/setup-micromamba](https://github.com/mamba-org/setup-micromamba) from 2 to 3.
- [Release notes](https://github.com/mamba-org/setup-micromamba/releases)
- [Commits](https://github.com/mamba-org/setup-micromamba/compare/v2...v3)

---
updated-dependencies:
- dependency-name: mamba-org/setup-micromamba
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-28 16:52:52 +11:00
dependabot[bot] 3d4db13fc1 Bump ruff from 0.15.7 to 0.15.8
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.7 to 0.15.8.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.7...0.15.8)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-28 16:52:46 +11:00
Thomas Krijnen 95b3f7dac4 Don't include dot as special when doing float runs 2026-03-27 21:04:48 +01:00
Thomas Krijnen 603cedc487 Try some things: (a) fewer allocations - parse context pool; lexer string pool (b) SWAR process multiple chars at once in keywords/enums/strs/stc. 2026-03-27 20:45:13 +01:00
Bruno Perdigão 31b571322b Snap: improve handling with objects that are partially behind the camera. 2026-03-27 15:05:02 -03:00
Bruno Perdigão cef5d41b54 Snap - Improves logic from previous commit.
Previous commit: Snap - Refactor x-ray mode handling
to prevent double raycasting
2026-03-27 15:05:02 -03:00
Bruno Perdigão d7b2358d58 Snap - Refactor x-ray mode handling to prevent double raycasting 2026-03-27 15:05:02 -03:00
Bruno Perdigão cafe5aa7f7 Rename variable - small refactor 2026-03-27 15:05:01 -03:00
Bruno Perdigão de34e73451 Remove unnecessary comments. 2026-03-27 15:05:01 -03:00
Bruno Perdigão 5721a8b602 Snap: improve performance of wireframe objects intersection.
Enhances the performance of mouse intersection checks for wireframe objects.
Details:
- Calculated the intersection with the mouse in 2D pixels first.
- Converted objects to a BVH Tree to reduce the number of edges checked against the mouse position.
2026-03-27 15:04:48 -03:00
Bruno Postle f820214500 ifcmcp: fail early with clear message when mcp package is not installed
mcp is an optional dependency so that the embedded API (embedded.py) can
be used from Pyodide without pulling in pydantic-core and the rest of the
MCP protocol stack, which may not be available in all WASM environments.
2026-03-27 08:44:52 +00:00
Thomas Krijnen fe6e9d86ae Xml serializer proper specialization 2026-03-26 15:49:54 +01:00
Thomas Krijnen 8e42f35db3 Rework variable length token storage to use string pool; eliminate need for rereads 2026-03-26 15:49:28 +01:00
Thomas Krijnen 9d69a712ac Does SWIG prefer std::conditional_t over auto return type? 2026-03-26 11:33:00 +01:00
Thomas Krijnen dc127471c5 Rerun codegen 2026-03-26 10:52:12 +01:00
Thomas Krijnen 95a094d596 Fix some schema generation issues 2026-03-26 10:39:29 +01:00
Bruno Postle dae913e06a ifcmcp: sse,streamable-http transports and --help 2026-03-26 07:02:50 +00:00
Dion Moult 1a849395c2 Typo crashing edit tools panel when non-wall with wall selected
Fix #7034

bpy.ops.bim.extend_to_underside doesn't exist - the correct operator
name is bim.extend_walls_to_underside. The AttributeError killed the
entire panel draw, hiding mirror, align, aggregation, and QTO buttons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 13:38:05 +11:00
Dion Moult 611273a20a Fix add_georeferencing silently failing with orphan CRS or conversion
If a file had an IfcProjectedCRS without an IfcCoordinateOperation (or
vice versa), add_georeferencing would return early without creating the
missing entity. This caused edit_georeferencing to crash with IndexError.
Now detects the inconsistent state, cleans up, and recreates both.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 15:00:32 +11:00
Bruno Postle db68195310 Add ifcmcp: MCP server for IFC model querying and editing (#7847)
ifcmcp is a new Model Context Protocol server that wraps ifcquery and ifcedit, holding an IFC model in memory across tool calls. It is the preferred way to interact with IFC models from AI assistants and MCP-compatible clients.

Setup:

claude mcp add --transport stdio ifc -- python3 -m ifcmcp

Session tools: ifc_load, ifc_save

Query tools: ifc_summary, ifc_tree, ifc_info, ifc_select, ifc_relations, ifc_clash, ifc_validate, ifc_schedule, ifc_cost, ifc_schema, ifc_contexts, ifc_materials, ifc_plot, ifc_render, ifc_shape, ifc_shape_list, ifc_shape_docs

Edit discovery: ifc_list, ifc_docs

Edit execution: ifc_edit, ifc_quantify

The model stays in memory between calls - ifc_edit does not auto-save; call ifc_save explicitly when done.

Depends on both ifcquery and ifcedit

Generated with the assistance of an AI coding tool.
2026-03-23 23:54:17 +00:00
Bruno Postle 29079e8cba ifcquery README: add contexts, materials, plot, render subcommands (#7848) 2026-03-23 23:51:33 +00:00
Bruno Postle 6bf4259298 Add ifcedit: CLI wrapper for ifcopenshell.api mutation functions (#7846)
ifcedit is a new command-line tool for executing ifcopenshell.api mutations from the shell. It wraps the entire API surface — any function callable via ifcopenshell.api can be invoked without writing Python.

Subcommands:

    list [module] — list all API modules, or functions within a module
    docs <module.function> — full documentation (params, types, descriptions)
    run <file> <module.function> [--param value ...] — execute a mutation; overwrites input file by default, or use -o <output> to write elsewhere; --dry-run validates without executing
    quantify list — list available QTO rules
    quantify run <file> <rule> — run quantity take-off, writing IfcElementQuantity psets back to the file

Parameter coercion: entity references can be passed as step IDs (strings); lists, dicts, booleans, and None are handled automatically.

Usage:

python3 -m ifcedit run model.ifc root.remove_product --product 42
python3 -m ifcedit docs geometry.edit_object_placement

Generated with the assistance of an AI coding tool.
2026-03-23 23:45:42 +00:00
Bruno Postle 7cd40bf8cb Add ifcquery CLI tool for IFC model interrogation (#7845)
ifcquery is a new command-line tool for querying and inspecting IFC models. All output is JSON.

Subcommands:

    summary — schema version, entity counts, project metadata
    tree — full spatial hierarchy (Project → Site → Building → Storeys → Spaces → Elements)
    info <id> — deep inspection of any entity by step ID (attributes, psets, placement matrix, type, material)
    select <query> — filter elements using ifcopenshell selector syntax
    relations <id> — relationships for an element; --traverse up walks to IfcProject
    clash <id> — geometric intersection and clearance detection
    validate — schema/constraint validation; --rules adds EXPRESS checks
    schedule — work schedules with nested task trees
    cost — cost schedules with nested cost item trees
    schema <class> — IFC class documentation from the model's schema version
    plot — SVG plan drawing
    render — 3D geometry rendering
    contexts — geometric representation contexts
    materials — material assignments

Usage:

python3 -m ifcquery <file.ifc> <subcommand> [args]

Generated with the assistance of an AI coding tool.
2026-03-23 23:29:32 +00:00
Bruno Postle 8b8f78095d geometry_creation.rst: add sections for assemblies, clipping normals, openings (#7844)
Generated with the assistance of an AI coding tool.
2026-03-23 23:02:15 +00:00
Bruno Postle 23ba9e4db0 Add geometry.clip_solid, clip_solid_bounded, and copy_representation APIs (#7843)
* Add geometry.clip_solid API
* Add geometry.clip_solid_bounded API
* Add geometry.copy_representation API
Deep-copies the named representation from a source element to a target
element.

Generated with the assistance of an AI coding tool.
2026-03-23 23:00:12 +00:00
Bruno Postle 1aec991f08 api: docstring improvements across geometry, sequence, and feature modules (#7842)
* Doc clarification for api.sequence.assign_process
* Doc clarification for api.geometry.edit_object_placement
* Doc clarification for api.feature.remove_feature
* Doc clarification for api.geometry.add_wall_representation clippings normal
* regenerate_wall_representation: document BBIM_Boolean preservation requirement

Generated with the assistance of an AI coding tool.
2026-03-23 22:57:28 +00:00
Bruno Postle bddf9b85f8 shape_builder: complete docstrings and return type annotations (#7841)
* shape_builder: complete docstrings and return type annotations
* shape_builder: warn about mixed item types in get_representation
* shape_builder: fix half_space_solid agreement_flag docstring

Generated with the assistance of an AI coding tool.
2026-03-23 22:54:55 +00:00
Sayan J. Das f679c63a18 Merge pull request #7808 from theseyan/ifctester-improvements-rebased
IfcTester webapp improvements
2026-03-23 15:48:34 +05:30
Thomas Krijnen e6cc0e7813 Initialize ncount_total #7834 2026-03-23 10:54:38 +01:00
Dion Moult cf2acfc649 Add covering feature tests for ceiling and cursor variants
Add tests for all four covering generation operators: flooring/ceiling
from walls and flooring/ceiling from cursor. Previously only flooring
from walls was tested.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 23:25:04 +11:00
Dion Moult 60ebb99fda Fix covering geometry not persisting to IFC, same root cause as #7055
The covering tool used bmesh as an intermediate and relied on
type.assign_type post-listeners (removed in 44a52863a) to generate
the IfcExtrudedAreaSolid body. With those listeners gone, coverings
had no body representation and assign_swept_area_outer_curve crashed.

Build covering representations from scratch using ShapeBuilder, reading
the extrusion depth from the type's IfcMaterialLayerSet. Also replace
bpy.ops.bim.assign_class with bonsai.core.root.assign_class using
should_add_representation=False, consistent with the space fix.

Refactored shared coordinate-conversion and extrusion-building logic
into get_2d_vertices_from_polygon and set_extrusion_representation_from_polygon,
used by both space and covering code paths. Removed all bmesh-dependent
dead code from the spatial tool.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 23:18:59 +11:00
Dion Moult ab96add772 Gitignore all test cache files 2026-03-22 22:26:23 +11:00
Dion Moult d8de623086 Fix space regen not saving geometry to IFC (#7055)
Space regeneration was only updating the Blender mesh and marking the
object as edited, but the IFC representation was never synced on save.
Replace the bmesh-based approach with ShapeBuilder to write geometry
directly to IFC as an IfcExtrudedAreaSolid, then reload via
switch_representation. This applies to both new space creation and
existing space regeneration.

Also changes assign_ifcspace_class_to_obj to call
bonsai.core.root.assign_class directly with
should_add_representation=False instead of bpy.ops.bim.assign_class.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 22:14:02 +11:00
dependabot[bot] a3f2e061fb Bump hendrikmuhs/ccache-action from 1.2.20 to 1.2.21
Bumps [hendrikmuhs/ccache-action](https://github.com/hendrikmuhs/ccache-action) from 1.2.20 to 1.2.21.
- [Release notes](https://github.com/hendrikmuhs/ccache-action/releases)
- [Commits](https://github.com/hendrikmuhs/ccache-action/compare/v1.2.20...v1.2.21)

---
updated-dependencies:
- dependency-name: hendrikmuhs/ccache-action
  dependency-version: 1.2.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-22 21:31:54 +11:00
dependabot[bot] f51a4673db Bump ruff from 0.15.6 to 0.15.7
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.6 to 0.15.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.6...0.15.7)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-22 21:31:42 +11:00
Dion Moult 75b8d4f218 Remove spatial containment and aggregation when nesting
The nest assign_object API now removes existing spatial containment and
aggregate relationships before creating the nest, matching the behavior
documented in its docstring and consistent with aggregate.assign_object.

Fix #7248

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 15:28:47 +11:00
Dion Moult b8136d4762 Prevent cyclic references when assigning nesting or aggregation
Walk up the full hierarchy via get_parent() in can_nest() and
can_aggregate() to reject assignments that would create a cycle.
Also reject self-assignment.

Fix #7248

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 15:28:47 +11:00
Dion Moult ecde429d36 Fix crash after undo of assign_class on macOS (#7419)
After assigning an IFC class and undoing, msgbus subscriptions registered
with the old Python object wrapper survived (PERSISTENT flag) but could
not be cleared because: (1) rollback_link_element looked up objects by
their post-link name which no longer exists after undo, and (2) the
per-object clear_by_owner calls in rebuild_element_maps used new Python
wrappers that didn't match the old subscription owners.

Fix by using a dedicated stable object (object_subscription_owner) as
the msgbus owner for all per-object subscriptions, allowing
rebuild_element_maps to clear all stale subscriptions in one call
regardless of Python wrapper identity changes during undo/redo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 14:42:55 +11:00
Dion Moult 1771b34449 Fix error when entering edit mode on camera objects
Fixes #7313.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 14:08:12 +11:00
Dion Moult 41469acbc8 Fix walrus operator precedence in MaterialCreator
The `is not ...` was being captured by the walrus assignment due to
missing parentheses, causing the condition to always evaluate incorrectly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 13:58:17 +11:00
Ryan Schultz 547b22199f Without 'Material.Name' layers merge. (#7700) 2026-03-21 18:02:02 -05:00
Dion Moult 94c15213f6 Guard against emptying IfcShapeRepresentation Items
remove_representation_item now returns early if removing the item would
leave Items empty. edit_text_literals returns early on empty attributes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 20:10:11 +11:00
Dion Moult fca258fb07 Fix add_boolean removing second operands from unrelated representations
add_boolean was removing second operands from ALL IfcShapeRepresentations
that referenced them, which could corrupt unrelated shapes and leave
representations with empty Items (bug #7803).

The API no longer modifies Items — callers manage this explicitly.
validate_type and Bonsai's AddBoolean operator now handle their own
item removal scoped to the correct representation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 20:10:11 +11:00
Dion Moult bcfad8d96d Migrate remove_deep to remove_deep2 across API modules
remove_deep is deprecated and can silently delete elements still in use.
remove_deep2 requires zero inverses before removal, making it safer.
Also fixes a double-removal bug in remove_grid_axis and prevents
removing the last prop template from a pset template.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 20:10:11 +11:00
Parag Debnath c026dd3b6e IsVentilated now defaults to False (#7819)
* IsVantillated now defaults to false

* IsVentilated now defaults to False

---------

Co-authored-by: Parag Debnath <paragforwork@gmail.com>
2026-03-20 23:33:40 +11:00
Dion Moult d0f20371bd Add feature to get parent of a particular IFC class 2026-03-20 23:10:00 +11:00
Dion Moult 7b6e82a9cc Fix stair calculated params test to set custom_tread_lock=False
Tests using custom first/last tread runs were not setting
custom_tread_lock=False, so the custom values were silently ignored
since 8f7cf76d9 introduced the lock gate in the calculation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 23:09:15 +11:00
Andrej730 286c69429d gitignore bonsai external_dependencies 2026-03-20 15:49:08 +05:00
dependabot[bot] 9c22dc6013 Bump socket.io-parser from 4.2.4 to 4.2.6 in /src/ifctester/webapp
Bumps [socket.io-parser](https://github.com/socketio/socket.io) from 4.2.4 to 4.2.6.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.4...socket.io-parser@4.2.6)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-version: 4.2.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 11:47:01 +01:00
Andrej730 0acbd5ffad black . 2026-03-20 15:45:24 +05:00
Andrej730 77f0c43314 ids_doc_generator - fix invalid escape sequence SyntaxWarning
SyntaxWarning: invalid escape sequence '\/' at line 312.
`\/` in a plain string is treated as `/` by accident; replaced with raw string r"..." to be explicit.
2026-03-20 15:43:13 +05:00
Andrej730 c1a9708504 ci.yml - build ifctester docs
to ensure script doesn't break
2026-03-20 15:43:13 +05:00
Andrej730 bcf6f6197d ifctester - move build-ids-docs target to ifctester Makefile
Also added a note why it lives in test folder and added it's output to gitignore.
2026-03-20 15:43:13 +05:00
Andrej730 1778656bd5 ids_doc_generator - fix Property args missed (ed8eb75)
TypeError: Property.__init__() got an unexpected keyword argument 'name'
2026-03-20 15:43:12 +05:00
Andrej730 54f450129c ids_doc_generator - fix failed_entities removed (bd92c043)
AttributeError: 'Attribute' object has no attribute 'failed_entities'
2026-03-20 15:43:12 +05:00
Andrej730 9fad1569c7 ids_doc_generator - fix error due to stale cache (f40281e97)
AssertionError: bool(facet(inst)) is expected
2026-03-20 15:43:12 +05:00
Andrej730 722c374fa6 ids_doc_generator - handle invalid entities coming from a test (1ed770d)
Exception: About to emit invalid example data: IfcMaterial.Name not optional
2026-03-20 15:43:12 +05:00
Andrej730 91b6c3e256 bcf v3 tests - fix wrong args, add dead code TODOs 2026-03-20 15:43:12 +05:00
Andrej730 ea3f71b4e0 rename test files to test_* prefix for pytest discovery and fix missing add_pset name arg 2026-03-20 15:43:12 +05:00
Andrej730 1a8b17e235 ifcfm cobie24 - remove unused ifc_file param from get_unit_name 2026-03-20 15:43:12 +05:00
Andrej730 2bad861122 ifcopenshell_wrapper.pyi - support varargs and kwargs in constructors 2026-03-20 15:43:11 +05:00
Andrej730 6c1fb3b01a Remove stale mass_time_units_in_wizard references (5c31ae4c3) 2026-03-20 15:36:18 +05:00
Andrej730 f5be64af6c Remove redundant __init__ from BaseLinesShader 2026-03-20 15:36:18 +05:00
Andrej730 b043dd4d04 Fix unknown-argument error in BaseLinesShader.__init__ 2026-03-20 15:36:18 +05:00
Andrej730 ffd2466321 Fix missing prop name in bim.mep_add_bend 2026-03-20 15:36:17 +05:00
Andrej730 e9241fd812 Fix error in bim.fit_flow_segments 2026-03-20 15:36:17 +05:00
Andrej730 48d45451ac Remove dead code join_walls_TZ, join_T, join_Z superseded in acdc40fb4 2026-03-20 15:36:17 +05:00
Andrej730 3b7cf6e865 Fix error displaying bsdd description after API update (ed81a0a4b) 2026-03-20 15:36:17 +05:00
Andrej730 6038373ee5 ifcopenshell_wrapper.pyi - sync default values, validate_stub - suggest default values 2026-03-20 15:36:16 +05:00
Andrej730 3d7de87b46 ifcopenshell_wrapper.pyi - add temp MakeVolume stub 2026-03-20 15:36:16 +05:00
Andrej730 cb113ae8da ifcopenshell_wrapper.pyi - support stubs for constructors 2026-03-20 15:36:15 +05:00
Andrej730 26280d24fe Add ty to check for missing symbols and other simple errors 2026-03-20 15:36:14 +05:00
Andrej730 30551cb288 typing 2026-03-20 15:36:14 +05:00
Andrej730 3bf0edeca2 Fix subtle walrus operator bug in align_walls using e before assignment 2026-03-20 15:34:57 +05:00
Andrej730 3590b08e68 search/operator - remove unnecessary Ifc Operators 2026-03-20 15:34:57 +05:00
Ryan Schultz fd902d88fb Update selector_syntax.rst with query examples
Clarified usage of queries in IfcAnnotation tags with examples.
2026-03-18 18:25:03 -05:00
Sayan Jyoti Das aa5f5120e0 delete ifcopenshell wheel 2026-03-18 14:37:24 +05:30
Sayan Jyoti Das 81986bcbfb ifcopenshell wasm wheel should be dynamically fetched, not included in git 2026-03-18 14:35:55 +05:30
Andrej730 ec6c268cdb Fix type assign_type core test (44a52863a) 2026-03-18 13:15:39 +05:00
Andrej730 64003fd5ef Fix drawing update_drawing_name core test (19534e225) 2026-03-18 13:15:38 +05:00
Andrej730 58d07bace4 Fix drawing edit_text core test and tool interface (5e9f97a0c) 2026-03-18 13:15:38 +05:00
Andrej730 3b718bc58d Fix georeference core tests (b246998f6) 2026-03-18 13:15:38 +05:00
tsomanna_QCOM 18c035ea77 Fix Windows ARM64 Python Bindings Issue 2026-03-18 08:44:34 +01:00
Andrej730 f2e2e324b1 Fixing stubs
- `function_item`, `tags` added in df7318973
- MakeVolume added in c385b93, ignore as all other conversion settings
- moved `SeparateZUpNode` ignore to the other geom serializer settings
2026-03-18 12:25:14 +05:00
Andrej730 069dbbd8c2 bonsai docs - add maintenance page 2026-03-18 12:25:14 +05:00
Andrej730 3385872e8b ci-black-formatting - use variables for min Python versions 2026-03-18 12:25:14 +05:00
Andrej730 f0b27a0910 ifcopenshell-python Makefile - simplify pyversion check, similar to 6409f41 2026-03-18 12:25:13 +05:00
Andrej730 888158570a Remove Python 3.9 references 2026-03-18 11:20:22 +05:00
Andrej730 c03156b5cd control.assign_control - remove deprecated related_object argument support 2026-03-18 11:20:22 +05:00
Andrej730 05bf2b82d2 system.disconnect_port - fix missing flow direction reset (bbda8d2) 2026-03-18 11:03:33 +05:00
Andrej730 7bdc1b6a75 cache_dependencies - skip ifcopenshell dir when packing 2026-03-18 11:01:38 +05:00
Sayan Jyoti Das 03de69814a fixes and cleanups from old branch 2026-03-18 11:22:41 +05:30
Sayan Jyoti Das 4dc6a0f2bc update ifcopenshell wheel to ifcopenshell-0.8.5+a51b2c5 2026-03-18 11:18:50 +05:30
Andrej730 c33509364c Fix error generating ifcpatch recipes docs for Bonsai tooltips
Mentioned in https://github.com/IfcOpenShell/IfcOpenShell/issues/7667#issuecomment-4076645173

Traceback:
```
Traceback (most recent call last):
  File "\bonsai\bim\module\patch\prop.py", line 55, in get_ifcpatch_recipes
    docs = ifcpatch.extract_docs(f, "Patcher", "__init__", ("src", "file", "logger", "args"))
  File "\ifcpatch\__init__.py", line 168, in extract_docs
    spec.loader.exec_module(submodule)
    ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
  File "<frozen importlib._bootstrap_external>", line 1027, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "\ifcpatch/recipes/FixRevit2025TINs.py", line 31, in <module>
    class Patcher:
    ...<509 lines>...
            return co / self.unit_scale
  File "\ifcpatch/recipes/FixRevit2025TINs.py", line 168, in Patcher
    def create_edges(self, obj: bpy.types.Object) -> None:
                                ^^^
NameError: name 'bpy' is not defined
File "\bonsai\bim\module\patch\prop.py", line 43, in get_ifcpatch_recipes
```
2026-03-18 10:04:36 +05:00
Sayan Jyoti Das 47f058341b local ifctester wheel build 2026-03-18 10:29:09 +05:30
Thomas Krijnen a51b2c587c Revert "Simplifies IfxAxis2PlacementLinear, assumes default Axis = (0,0,1)"
This reverts commit cf1552e79e.
2026-03-17 20:49:48 +01:00
Sayan Jyoti Das 845a13ba83 some fixes and lint cleanups 2026-03-17 21:16:53 +05:30
Sayan Jyoti Das 530841967e Merge branch 'v0.8.0' into ifctester-improvements 2026-03-17 19:49:09 +05:30
Andrej730 c36e7badae Remove use of deprecated os.popen 2026-03-17 18:14:22 +05:00
Andrej730 515fe8d2ef Remove use of deprecated tempfile.mktemp 2026-03-17 18:14:22 +05:00
Andrej730 b8d3d1d105 pyproject.toml - add ty command to check for deprecated methods 2026-03-17 18:14:22 +05:00
Sayan Jyoti Das e95da857d6 convert codebase to typescript + introduce biome lint 2026-03-16 21:55:10 +05:30
Sayan Jyoti Das d587d1ac11 build step for pyodide 2026-03-16 21:46:46 +05:30
Andrej730 ba36dc82ff bim.clear_measurement - add poll message 2026-03-16 15:27:39 +05:00
Andrej730 025fb769e2 bim.explore_tool - remove additional row to keep hotkey and operators on the same row 2026-03-16 15:27:39 +05:00
Andrej730 bf75a19640 bim.image_scaling_tool - break description to multiple lines for readibility 2026-03-16 15:27:39 +05:00
Andrej730 4473dbd138 bim.generate_uv_map - move to operator.py, fix missing description, add separate row in ui 2026-03-16 15:27:39 +05:00
Sayan Jyoti Das 6117417b89 update webapp + bonsai integration 2026-03-16 15:54:52 +05:30
Thomas Krijnen 0398584c69 empty 2026-03-16 15:45:41 +05:30
Thomas Krijnen 0c69f85d5e Empty 2026-03-16 15:45:40 +05:30
Andrej730 7e987be00f bim.link_ifc - document default query
To make it more discoverable for users.
2026-03-16 15:10:05 +05:00
Andrej730 35e3d9c42e Linked Models - invalidate cache for mismatching query automatically 2026-03-16 15:10:04 +05:00
Andrej730 63a8639353 Linked Models - option to provide custom selector query
Available in file dialog when linking model - https://files.catbox.moe/tdmmbt.png
It's not very robust currently, just something to start with.
2026-03-16 15:10:04 +05:00
Andrej730 bd15ba4aa3 Linked Models - fix removing link operator missing if link is still loaded 2026-03-16 15:10:04 +05:00
Andrej730 f583d1ecc1 typing 2026-03-16 15:10:04 +05:00
Andrej730 5bfab569ba bim.link_ifc - fix prop display in file dialog panel
Fixes this - https://files.catbox.moe/eq10ip.png
2026-03-16 15:10:04 +05:00
Andrej730 fb16e91249 Remove use of deprecated datetime.utcnow()
To fix warnings below:
```
<python-input-1>:1: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
```
2026-03-16 15:10:03 +05:00
Andrej730 1c456c3cb2 Bonsai Makefile - use official bpypolyskel repo instead of fork
Since https://github.com/prochitecture/bpypolyskel/pull/22 got merged.
2026-03-16 15:10:03 +05:00
Andrej730 a8d28fb469 Remove unused import, black . 2026-03-16 15:10:03 +05:00
Dion Moult 62bb6cdf33 Fix failing classification tests because they relied on spaces which are now hidden by default 2026-03-16 19:31:25 +11:00
Dion Moult cf153981ce Feature tests for add/remove literal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 19:04:35 +11:00
Dion Moult b38316336c Revert "Fix #6392: when duplicating a window/door/etc, the associated IfcOpenElement duplicates as well."
This reverts commit a2a5780d59.
2026-03-16 17:52:02 +11:00
Dion Moult 021e6b9ef5 Simplify get model types to just got all type products. Fixes failing test. 2026-03-16 15:15:12 +11:00
falken10vdl 1999d93f9a Add GenerateUVMap operator and integrate into ExploreTool (#7695)
Co-authored-by: Dion Moult <dionmoult@gmail.com>
2026-03-16 07:30:49 +11:00
Dirk Olbrich 8c9e89ace8 Bonsai - change add_grid operator namespace to bim 2026-03-15 23:50:52 +11:00
Ryan Schultz 868bb5c39e Allow bulk annotation product assignment
Closes #7787: Previously bim.assign_selected_as_product required exactly
2 objects. With multiple annotations referencing the same
product, users had to repeat the operation once per
annotation. Now any number of IfcAnnotations can be selected
alongside a single product object and all are assigned in
one operation and one undo step.

Generated with the assistance of an AI coding tool.
2026-03-15 23:42:07 +11:00
Dion Moult c3a87c8f9c Add basic text editing feature tests 2026-03-15 23:30:10 +11:00
Dion Moult c30d24c4d9 Fix regression where changing logic to occur in filesystem selector caused headless test to fail.
See d4388ec76
2026-03-15 23:29:57 +11:00
Dion Moult 2edd1a5044 Black 2026-03-15 21:39:57 +11:00
Dion Moult 08dfcea47c Fix Python signatures in operator descriptions.
Closes #7797. Closes #7230.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 21:38:20 +11:00
Dion Moult 9c8f25739c Fix failing test. Add reference images should use generated coords, not UV.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 19:27:42 +11:00
Sebastian Schilling 256e4d2191 buildingSMART Data Dictionary module: use pSets from different data dictionary sources (#7764)
* buildingSMART Data Dictionary module: added textfield to change data dictionary url

* moved change of bsdd baseurl change to addon settings

* Receiving Psets from other dictionary sources has been made available by dynamizing the  identifier_url using the client baseurl

* Remove unnecessary blank lines in prop.py

* Remove unused import of bsdd module
2026-03-15 12:56:04 +11:00
Dion Moult b14da14614 Default to assigning material set usages if assigning to an occurrence. See #7794.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 12:26:21 +11:00
Dion Moult 273ecfe8e4 Supersede 3x3 box alignment with more familiar horizontal / vertical UI
* Fix #7712 - global alignment controls now affects all literals
 * Fix #7760 - goodbye 3x3 box alignment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 08:24:38 +11:00
Dion Moult 6e0f24105e Minor fix to regression in 95480a2 where reshaping to a 3x3 matrix was removed 2026-03-15 07:28:38 +11:00
Ryan Schultz 25af50a092 Temp files from ai coding tools 2026-03-15 07:20:15 +11:00
dependabot[bot] 4b5a50a831 Bump actions/download-artifact from 8.0.0 to 8.0.1
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v8.0.0...v8.0.1)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-15 07:19:11 +11:00
dependabot[bot] bd77175c66 Bump ruff from 0.15.5 to 0.15.6
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.5 to 0.15.6.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.5...0.15.6)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-15 07:19:05 +11:00
dependabot[bot] 2b1d99a8b1 Bump gersemi from 0.26.0 to 0.26.1
Bumps [gersemi](https://github.com/BlankSpruce/gersemi) from 0.26.0 to 0.26.1.
- [Release notes](https://github.com/BlankSpruce/gersemi/releases)
- [Changelog](https://github.com/BlankSpruce/gersemi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BlankSpruce/gersemi/compare/0.26.0...0.26.1)

---
updated-dependencies:
- dependency-name: gersemi
  dependency-version: 0.26.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-15 07:18:58 +11:00
Dion Moult 82adf4d18c Fix #7782: Don't allow assigning styles if no styles available.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 07:03:48 +11:00
Thomas Krijnen cf9df33aa5 Change checkout reference to build branch 2026-03-14 14:51:24 +01:00
Thomas Krijnen 1a6fd2530f Remove dependency on Standard_failure #7788 2026-03-14 14:44:47 +01:00
Dion Moult ea64f1b6b9 Fix #7770, #7747, #7572, #7522, #7416, #7086: Bug when first point in poly tool clicked twice
Previous logic always skipped the first point. Instead, it should only
skip when actually closing a loop (i.e. >= 3 points).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 20:35:59 +11:00
Dion Moult 473d689cf2 Fix #7794: Only slice layerset mesh when material layer set usage exists
Without a usage, orientation is undefined so slicing should be skipped.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 13:25:14 +11:00
Richard Brice cf1552e79e Simplifies IfxAxis2PlacementLinear, assumes default Axis = (0,0,1) 2026-03-13 11:16:10 -07:00
Andrej730 90db564b4b ifcclash - add advanced package type that supports smart group clashes 2026-03-13 20:26:23 +05:00
Andrej730 e9fb6b3c1d Bonsai Makefile - upstream bpypolyskel wheel recipe
Sent PR - https://github.com/prochitecture/bpypolyskel/pull/22
2026-03-13 20:26:23 +05:00
Andrej730 a5e0b3f0e5 Bonsai Makefile - upstream ifcjson wheel recipe
Sent PR - https://github.com/IFCJSON-Team/IFC2JSON_python/pull/8
2026-03-13 20:26:23 +05:00
Andrej730 60519e8fed ifcopenshell dev_environment - include all packages 2026-03-13 20:26:23 +05:00
Andrej730 8fb454bb27 Partially disable old Windows workaround for Bonsai uninstallation 2026-03-13 20:26:23 +05:00
Andrej730 9ed8f3e244 Bonsai - fix missing Bonsai Fatal Error UI
Since we added more data to debug info in fcf5614 Fatal Error itself started to fail and was never displayed due some props being inaccessible during load, should be fixed now.

Possible error that were fixed:
```
  File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 328, in <module>
    print(format_debug_info(get_debug_info()))
                            ~~~~~~~~~~~~~~^^
  File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 117, in get_debug_info
    if bpy.data.is_saved:
       ^^^^^^^^^^^^^^^^^
AttributeError: '_RestrictData' object has no attribute 'is_saved'

Traceback (most recent call last):
  File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 366, in draw
    info = get_debug_info()
  File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 152, in get_debug_info
    bim_props = tool.Blender.get_bim_props()
                ^^^^
NameError: name 'tool' is not defined. Did you mean: 'bool'?

Traceback (most recent call last):
  File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 366, in draw
    info = get_debug_info()
  File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 141, in get_debug_info
    import bonsai.tool as tool
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\__init__.py", line 355, in <module>
    print(format_debug_info(get_debug_info()))
                            ~~~~~~~~~~~~~~^^
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\__init__.py", line 141, in get_debug_info
    import bonsai.tool as tool
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\tool\__init__.py", line 23, in <module>
    from bonsai.tool.attribute import Attribute
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\tool\attribute.py", line 31, in <module>
    import bonsai.bim.helper as helper
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\bim\__init__.py", line 28, in <module>
    from . import handler, operator, prop, ui
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\bim\handler.py", line 36, in <module>
    from bonsai.bim.module.aggregate.decorator import AggregateDecorator
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\bim\module\aggregate\__init__.py", line 21, in <module>
    from . import operator, prop, ui
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\bim\module\aggregate\operator.py", line 32, in <module>
    class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
                                                             ^^^^^^^^
AttributeError: partially initialized module 'bonsai.tool' from '\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\tool\__init__.py' has no attribute 'Ifc' (most likely due to a circular import)
```
2026-03-13 20:26:23 +05:00
Andrej730 298beacef6 Bonsai - remove pyperclip use 2026-03-13 20:26:22 +05:00
Andrej730 eba798c544 typing 2026-03-13 20:26:22 +05:00
dependabot[bot] 1576302caf Bump devalue from 5.6.3 to 5.6.4 in /src/ifctester/webapp
Bumps [devalue](https://github.com/sveltejs/devalue) from 5.6.3 to 5.6.4.
- [Release notes](https://github.com/sveltejs/devalue/releases)
- [Changelog](https://github.com/sveltejs/devalue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/devalue/compare/v5.6.3...v5.6.4)

---
updated-dependencies:
- dependency-name: devalue
  dependency-version: 5.6.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-13 07:58:26 +01:00
dependabot[bot] d7f29c4494 Bump tar from 7.5.10 to 7.5.11 in /src/ifctester/webapp
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.10 to 7.5.11.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.10...v7.5.11)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-13 07:58:22 +01:00
dependabot[bot] 4ff6f7c78b Bump black from 26.3.0 to 26.3.1
Bumps [black](https://github.com/psf/black) from 26.3.0 to 26.3.1.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/26.3.0...26.3.1)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 26.3.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-13 07:58:06 +01:00
Andrej730 0b44e146bd Fix error starting Bonsai (77697aeef) 2026-03-13 11:57:17 +05:00
tsomanna_QCOM cf42f986c4 Add Support for IfcOpenShell on Win ARM64 2026-03-12 20:29:10 +01:00
tsomanna_QCOM e84142894e Add Support for IfcOpenShell on Win ARM64 2026-03-12 20:29:10 +01:00
tsomanna_QCOM b38a0dce1f Add Support for IfcOpenShell on Win ARM64 2026-03-12 20:29:10 +01:00
tsomanna_QCOM ae253f5e2a Add Support for IfcOpenShell on Win ARM64 2026-03-12 20:29:10 +01:00
tsomanna_QCOM b25c24a007 Add Support for IfcOpenShell on Win ARM64 2026-03-12 20:29:10 +01:00
Andrej730 8bd3fab608 Linked Models - option to isolate selected object
Quick demo - https://files.catbox.moe/6chdx7.mp4
2026-03-12 20:21:10 +05:00
Andrej730 82951fe702 Pylance - ignore _deps folder
In my case it was adding 3115 Python files and VS Code kept indexing them.
2026-03-12 20:21:10 +05:00
Andrej730 77697aeefc typing 2026-03-12 20:21:10 +05:00
Thomas Krijnen 71442bd4d4 Submodule 2026-03-12 12:34:05 +01:00
Andrej730 f7bee258c6 Linked models - add description for georeferencing indicator
Example - https://files.catbox.moe/bkn9pn.png
2026-03-11 18:38:25 +05:00
Andrej730 0585f8716a Quick Favorites Manager - support enum items 2026-03-11 18:38:25 +05:00
Andrej730 f3fe1a7bf0 ci.yml - BUILD_EXAMPLES=ON to keep testing examples build 2026-03-11 18:38:25 +05:00
Andrej730 61ba6a7989 Add operator to run search generic search queries
So it will be easy to add queries to quick favorites.
2026-03-11 18:38:25 +05:00
Andrej730 97f900e62f Quick Favorites Manager - show operators suggestions 2026-03-11 18:38:25 +05:00
Andrej730 594d72d7e1 Quick Favorites Manager
Blender doesn't have it's own quick favorites manager and working with them can be not very flexible - you can add them in context menu and remove them from Quick Favorites menu. But you can't reorder them, you can't rename them and you can't even add a new button to favorites if it's not added by some addon in the UI.

Have been stumbling upon this for awhile and decided to create an experimental manager UI for this. Things it can do:
- help user create a button with any operator in Blender and properties they prefer to then save it Quick Favorites. Which seems can be very useful in Bonsai, since you can create separate buttons for all kinds of selectors expressions, class assignment or other operators.

- it can import quick favorites from user's actual current quick favorites, so they can just modify them a bit, reorder, rename and then add them again.

- Since quick favorites are not exposed to Python API in Blender, we're using a very hacky way to retrieve them from Blender and don't provide our own buttons for adding and removing quick favorites, as it may be dangerous and even more hacky in implementation. So the workflow for user is to either generate some buttons and add them to quick favorites using Manager or to import it's own quick favorites, then change them how they like, then remove quick favorites using usual quick favorites menu and then add new button one by one.

Small demo - https://files.catbox.moe/vyffp6.mp4
2026-03-11 18:38:24 +05:00
Andrej730 578caaf2cd tool.Blender.update_all_viewports 2026-03-11 18:38:24 +05:00
Andrej730 15ea092ac4 typing 2026-03-11 18:38:23 +05:00
Andrej730 4b12b6dacd bim.hide_queried_linked_element - note known UNDO limitation 2026-03-11 18:38:23 +05:00
Andrej730 584bbe3b83 tool/test_project - remove redundant __init__ 2026-03-11 18:38:22 +05:00
Andrej730 e47b7bca4b ci-black-formatting.yml - note on Python versions used 2026-03-11 18:38:22 +05:00
Andrej730 62b108766b ci-black-formatting.yml - note on Python versions used 2026-03-11 18:38:22 +05:00
HugoBallee 972a1fd309 Update getting_started.rst
IFC_SCHEMA_NAME matching includes
2026-03-10 09:41:09 +01:00
Bruno Postle 6f0bb21a43 Disable building example applications by default, closes #7763
Enable with -DBUILD_EXAMPLES=ON
2026-03-09 22:59:34 +00:00
dependabot[bot] 72f279381f Bump immutable from 5.1.2 to 5.1.5 in /src/ifctester/webapp
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 5.1.2 to 5.1.5.
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v5.1.2...v5.1.5)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 5.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 14:33:44 +01:00
dependabot[bot] 9873e403bc Bump tar from 7.5.9 to 7.5.10 in /src/ifctester/webapp
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.9 to 7.5.10.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.9...v7.5.10)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 14:33:38 +01:00
dependabot[bot] a48449bc9c Bump docker/setup-buildx-action from 3 to 4
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 14:33:31 +01:00
dependabot[bot] 8cc50b7399 Bump docker/build-push-action from 6 to 7
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 14:33:25 +01:00
dependabot[bot] da027b912e Bump black from 26.1.0 to 26.3.0
Bumps [black](https://github.com/psf/black) from 26.1.0 to 26.3.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/26.1.0...26.3.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 26.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 14:33:18 +01:00
dependabot[bot] b65a69b9ca Bump docker/login-action from 3 to 4
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 14:25:36 +01:00
dependabot[bot] 3b3b1f1eab Bump ruff from 0.15.4 to 0.15.5
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.4 to 0.15.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.4...0.15.5)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 14:25:11 +01:00
dependabot[bot] ff1b74ef10 Bump docker/setup-qemu-action from 3 to 4
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 14:22:05 +01:00
Richard Brice 610d639c21 Fixes rotation lerp test in make_loft 2026-03-08 10:37:05 -07:00
Richard Brice e282a18474 Fixes crash in taxonomy::loft::print_impl when axis == nullptr 2026-03-08 09:55:47 -07:00
Thomas Krijnen 0469a9528b use basic casts to prevent needless item upgrades #7738 2026-03-06 16:34:12 +01:00
Andrej730 f9486be172 get_linked_element_geom_slice - add tests 2026-03-06 19:07:34 +05:00
Andrej730 3a59425a64 Linked IFC models - hotkey to hide selected geometry
Demo - https://files.catbox.moe/aok74w.mp4
2026-03-06 19:07:34 +05:00
Andrej730 9d0c172a53 bim.select_linked_model_element
Refactored methods for accessing objects in linked models and added a simple operator to select object in linked model by providing guid.

A quick demo - https://files.catbox.moe/sjjw37.mp4
2026-03-06 19:07:34 +05:00
Andrej730 ecc82a52f5 ExtractPropertiesToSQLite - add typing for created columns 2026-03-06 19:07:34 +05:00
Andrej730 adaf33b74f project.operator - reuse ray_cast method 2026-03-06 19:07:33 +05:00
Andrej730 a52a329197 Update note on Blender upstream issue
Fix was included in 4.5.7 (see 141496 bug in https://projects.blender.org/blender/blender/issues/141871)
2026-03-06 19:07:33 +05:00
Andrej730 3a54e808f6 dev_environment python - create user site packages folder if missing
E.g. it might be missing if Python was just installed. Also print paths first before symlinking, making it easier to debug.
2026-03-06 19:07:33 +05:00
Andrej730 f102c7c1b4 ifcopenshell-python makefile - add note about PYNUMBER 2026-03-06 19:07:33 +05:00
Andrej730 9005333f53 ifcpatch MergeProjects - make logger arg optional 2026-03-06 19:07:33 +05:00
Andrej730 a26dbe252a bim.append_inspected_linked_element - fix missing UNDO 2026-03-06 19:07:33 +05:00
Andrej730 8743d5643e typing 2026-03-06 19:07:33 +05:00
Andrej730 619848823c Sort out imports 2026-03-06 19:07:32 +05:00
Richard Brice 951ade4b57 Fix bug introduced in 65d5df78 2026-03-05 14:50:33 -08:00
Richard Brice 1378919709 Fixes IfcLinearPlacement fallback position warning 2026-03-03 13:16:55 -08:00
Ryan Schultz 61cfa48c2c docs: add BonsaiPR bleeding edge installation section (#7721)
* Fix #7718: Fix FallDecorator label calculation for all slope annotation types

- Fix wrong dict key type in decoration.py: DecoratorData.data["fall"] is
  keyed by obj.name (str) but was looked up with obj (Object), causing
  object_type to always be None
- Apply obj.matrix_world transform to spline points before computing rise/run
  in both decoration.py and svgwriter.py; local coordinates have Z=0 for flat
  annotations, world coordinates correctly reflect elevation change
- Use hypotenuse (segment_length) instead of run as the denominator for
  SLOPE_FRACTION label display

Generated with the assistance of an AI coding tool.

* docs: add BonsaiPR bleeding edge installation section

Add new section to installation.rst documenting the BonsaiPR
community build, including why it exists, how the automated
PR-merging system works, installation steps with automated
updates, manual installation, and the PR workflow for
contributors.

Generated with the assistance of an AI coding tool.

* whoops
2026-03-03 18:48:03 +11:00
falken10vdl d6c782aba5 Add newline handling with add_newline_between_words n SvgWriter for text literals 2026-03-03 18:46:14 +11:00
dependabot[bot] d4150e0558 Bump svelte from 5.53.0 to 5.53.6 in /src/ifctester/webapp
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.53.0 to 5.53.6.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.53.6/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.53.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 18:44:47 +11:00
dependabot[bot] 4f6051cb0a Bump ruff from 0.15.2 to 0.15.4
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.2 to 0.15.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.2...0.15.4)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 18:44:40 +11:00
dependabot[bot] 5a27ec9814 Bump actions/upload-artifact from 6 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 18:44:32 +11:00
dependabot[bot] 0ce60f5061 Bump actions/download-artifact from 7.0.0 to 8.0.0
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7.0.0 to 8.0.0.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7.0.0...v8.0.0)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 18:44:25 +11:00
Thomas Krijnen 43146530b0 arrange polygons: Alternative (unused) perimiter approach; simpler topology handling; projection-based clean-up 2026-03-02 21:49:05 +01:00
Andrej730 db377e2178 Also bump binary version 2026-02-27 15:20:18 +05:00
Thomas Krijnen 5b0511379b IfcAxis1Placement.Axis is optional #7728 2026-02-27 11:06:45 +01:00
Andrej730 f8663b5e2b Bump ifcopenshell build
Just because it didn't happened for a while now and we need to test it.
2026-02-27 14:52:56 +05:00
Andrej730 92c979fbbf black . 2026-02-27 14:52:55 +05:00
Andrej730 8834a51122 format cmake files 2026-02-27 14:52:55 +05:00
Andrej730 1c5b825d8e build-all-win - fix missing compression for Python zip archives
Same as 5ebd425, should resolve https://github.com/ifcopenshell/ifcopenshell/issues/7404
2026-02-27 12:09:34 +05:00
dependabot[bot] 58c69f9d35 Bump ruff from 0.15.1 to 0.15.2
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.1 to 0.15.2.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.1...0.15.2)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-26 13:15:49 +01:00
dependabot[bot] 198e111a92 Bump gersemi from 0.25.4 to 0.26.0
Bumps [gersemi](https://github.com/BlankSpruce/gersemi) from 0.25.4 to 0.26.0.
- [Release notes](https://github.com/BlankSpruce/gersemi/releases)
- [Changelog](https://github.com/BlankSpruce/gersemi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BlankSpruce/gersemi/compare/0.25.4...0.26.0)

---
updated-dependencies:
- dependency-name: gersemi
  dependency-version: 0.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-26 13:15:39 +01:00
dependabot[bot] 35886c9f72 Bump svelte from 5.33.10 to 5.53.0 in /src/ifctester/webapp
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.33.10 to 5.53.0.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.53.0/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.53.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-26 13:14:38 +01:00
dependabot[bot] 097c8af7c7 Bump rollup from 4.41.1 to 4.59.0 in /src/ifctester/webapp
Bumps [rollup](https://github.com/rollup/rollup) from 4.41.1 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.41.1...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-26 13:14:21 +01:00
Andrej730 cfea3de552 cmake - fix msvc warning on linking without /ltcg flag
Linking flags were missing for `MODULE` type libraries, example warning: `IfcPythonPYTHON_wrap.obj : MSIL .netmodule or module compiled with /GL found; restarting link with /LTCG; add /LTCG to the link command line to improve linker performance`
2026-02-26 17:12:55 +05:00
Andrej730 5b86eedb26 cmake - fix ifc geom mapping not linking against IfcGeom library 2026-02-26 17:12:55 +05:00
Andrej730 fc13bcd055 bump ccache-action 2026-02-26 17:12:55 +05:00
Andrej730 be4806471d cache_dependencies - use tar instead of tarfile for archiving 2026-02-26 17:12:55 +05:00
Andrej730 7909997d42 build workflows - reuse cache_dependencies.py 2026-02-26 17:12:54 +05:00
Andrej730 00cd0b76f9 .gersemirc - search src for definitions
To fix errors when parsing custom macro from `src\examples\CMakeLists.txt`, see https://github.com/BlankSpruce/gersemi/issues/105
2026-02-26 17:12:54 +05:00
Andrej730 aa7710dd74 cache_dependencies.py - note expected cwd 2026-02-26 17:12:54 +05:00
Andrej730 fc7d15324f build-all - don't use main repo pyproject.toml for wasm builds 2026-02-26 17:12:54 +05:00
Andrej730 f8f4725054 build-all - remove wasm cxx flags workaround
As issue is now fixed upstream (https://github.com/pyodide/pyodide-build/issues/251)
2026-02-26 17:12:54 +05:00
Andrej730 333b6210a4 black . 2026-02-26 17:12:52 +05:00
Andrej730 ff3933a117 Remove some unused imports 2026-02-26 17:12:46 +05:00
Andrej730 34ffaea2c9 cmake - link serializers against IfcGeom to fix wasm build
jsonserializer is using ifcgeom and also eigen3
2026-02-26 17:12:45 +05:00
Andrej730 526b9537a9 build_pyodide.sh - allow executing multiple times 2026-02-26 17:12:45 +05:00
Andrej730 fb1c9eb7e3 build-all - fix missing f-string 2026-02-26 17:12:45 +05:00
Andrej730 a61d5a12fb build-all - use cmake to build swig
To keep it in sync with Windows build. Also Removed pcre2 dependency as apparently it's not required - we were not using it on Windows.
2026-02-26 17:12:45 +05:00
Andrej730 e54d16ef57 build_osx - ensure we use bison from brew instead of the default one 2026-02-26 17:12:45 +05:00
Andrej730 4591b6d926 cmake - ignore rocksdb shared library
If makes code target it by default if it's available, leading to errors below, since we don't really support using shared rocksdb. See some more details in the code comment.

IfcPythonPYTHON_wrap.cxx.obj : error LNK2001: unresolved external symbol "public: __cdecl rocksdb::Cleanable::~Cleanable(void)" (??1Cleanable@rocksdb@@QEAA@XZ)
IfcPythonPYTHON_wrap.cxx.obj : error LNK2001: unresolved external symbol "public: __cdecl rocksdb::Cleanable::Cleanable(void)" (??0Cleanable@rocksdb@@QEAA@XZ)
IfcPythonPYTHON_wrap.cxx.obj : error LNK2001: unresolved external symbol "public: class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > __cdecl rocksdb::Slice::ToString(bool)const " (?ToString@Slice@rocksdb@@QEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@_N@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "const rocksdb::WriteBatch::`vftable'" (??_7WriteBatch@rocksdb@@6B@)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual __cdecl rocksdb::WriteBatch::~WriteBatch(void)" (??1WriteBatch@rocksdb@@UEAA@XZ)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: __cdecl rocksdb::WriteBatch::WriteBatch(unsigned __int64,unsigned __int64,unsigned __int64,unsigned __int64)" (??0WriteBatch@rocksdb@@QEAA@_K000@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: __cdecl rocksdb::ColumnFamilyOptions::ColumnFamilyOptions(void)" (??0ColumnFamilyOptions@rocksdb@@QEAA@XZ)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > __cdecl rocksdb::Configurable::GetOptionName(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &)const " (?GetOptionName@Configurable@rocksdb@@MEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@AEBV34@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > __cdecl rocksdb::Configurable::SerializeOptions(struct rocksdb::ConfigOptions const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &)const " (?SerializeOptions@Configurable@rocksdb@@MEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@AEBUConfigOptions@2@AEBV34@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual bool __cdecl rocksdb::Configurable::OptionsAreEqual(struct rocksdb::ConfigOptions const &,class rocksdb::OptionTypeInfo const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &,void const * const,void const * const,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > *)const " (?OptionsAreEqual@Configurable@rocksdb@@MEBA_NAEBUConfigOptions@2@AEBVOptionTypeInfo@2@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QEBX3PEAV56@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual class rocksdb::Status __cdecl rocksdb::Configurable::ParseOption(struct rocksdb::ConfigOptions const &,class rocksdb::OptionTypeInfo const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &,void *)" (?ParseOption@Configurable@rocksdb@@MEAA?AVStatus@2@AEBUConfigOptions@2@AEBVOptionTypeInfo@2@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@2PEAX@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual class rocksdb::Status __cdecl rocksdb::Configurable::ConfigureOptions(struct rocksdb::ConfigOptions const &,class std::unordered_map<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >,struct std::hash<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > >,struct std::equal_to<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > >,class std::allocator<struct std::pair<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const ,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > > > > const &,class std::unordered_map<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >,struct std::hash<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > >,struct std::equal_to<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > >,class std::allocator<struct std::pair<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const ,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > > > > *)" (?ConfigureOptions@Configurable@rocksdb@@MEAA?AVStatus@2@AEBUConfigOptions@2@AEBV?$unordered_map@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@V12@U?$hash@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@2@U?$equal_to@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@2@V?$allocator@U?$pair@$$CBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@V12@@std@@@2@@std@@PEAV56@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual class rocksdb::Status __cdecl rocksdb::Configurable::ParseStringOptions(struct rocksdb::ConfigOptions const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &)" (?ParseStringOptions@Configurable@rocksdb@@MEAA?AVStatus@2@AEBUConfigOptions@2@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual void const * __cdecl rocksdb::Configurable::GetOptionsPtr(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &)const " (?GetOptionsPtr@Configurable@rocksdb@@MEBAPEBXAEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual class rocksdb::Status __cdecl rocksdb::Configurable::ValidateOptions(struct rocksdb::DBOptions const &,struct rocksdb::ColumnFamilyOptions const &)const " (?ValidateOptions@Configurable@rocksdb@@UEBA?AVStatus@2@AEBUDBOptions@2@AEBUColumnFamilyOptions@2@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual class rocksdb::Status __cdecl rocksdb::Configurable::PrepareOptions(struct rocksdb::ConfigOptions const &)" (?PrepareOptions@Configurable@rocksdb@@UEAA?AVStatus@2@AEBUConfigOptions@2@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual bool __cdecl rocksdb::Configurable::AreEquivalent(struct rocksdb::ConfigOptions const &,class rocksdb::Configurable const *,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > *)const " (?AreEquivalent@Configurable@rocksdb@@UEBA_NAEBUConfigOptions@2@PEBV12@PEAV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual class rocksdb::Status __cdecl rocksdb::Configurable::GetOption(struct rocksdb::ConfigOptions const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > *)const " (?GetOption@Configurable@rocksdb@@UEBA?AVStatus@2@AEBUConfigOptions@2@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@PEAV56@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "class rocksdb::TableFactory * __cdecl rocksdb::NewBlockBasedTableFactory(struct rocksdb::BlockBasedTableOptions const &)" (?NewBlockBasedTableFactory@rocksdb@@YAPEAVTableFactory@1@AEBUBlockBasedTableOptions@1@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: class std::shared_ptr<class rocksdb::Cache> __cdecl rocksdb::LRUCacheOptions::MakeSharedCache(void)const " (?MakeSharedCache@LRUCacheOptions@rocksdb@@QEBA?AV?$shared_ptr@VCache@rocksdb@@@std@@XZ)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: static class rocksdb::Status __cdecl rocksdb::DB::OpenForReadOnly(struct rocksdb::Options const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &,class std::unique_ptr<class rocksdb::DB,struct std::default_delete<class rocksdb::DB> > *,bool)" (?OpenForReadOnly@DB@rocksdb@@SA?AVStatus@2@AEBUOptions@2@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@PEAV?$unique_ptr@VDB@rocksdb@@U?$default_delete@VDB@rocksdb@@@std@@@6@_N@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: static class rocksdb::Status __cdecl rocksdb::DB::Open(struct rocksdb::Options const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &,class std::unique_ptr<class rocksdb::DB,struct std::default_delete<class rocksdb::DB> > *)" (?Open@DB@rocksdb@@SA?AVStatus@2@AEBUOptions@2@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@PEAV?$unique_ptr@VDB@rocksdb@@U?$default_delete@VDB@rocksdb@@@std@@@6@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "class std::vector<enum rocksdb::CompressionType,class std::allocator<enum rocksdb::CompressionType> > const & __cdecl rocksdb::GetSupportedCompressions(void)" (?GetSupportedCompressions@rocksdb@@YAAEBV?$vector@W4CompressionType@rocksdb@@V?$allocator@W4CompressionType@rocksdb@@@std@@@std@@XZ)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual bool __cdecl rocksdb::MergeOperator::PartialMergeMulti(class rocksdb::Slice const &,class std::deque<class rocksdb::Slice,class std::allocator<class rocksdb::Slice> > const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > *,class rocksdb::Logger *)const " (?PartialMergeMulti@MergeOperator@rocksdb@@UEBA_NAEBVSlice@2@AEBV?$deque@VSlice@rocksdb@@V?$allocator@VSlice@rocksdb@@@std@@@std@@PEAV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@5@PEAVLogger@2@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual bool __cdecl rocksdb::MergeOperator::FullMergeV3(struct rocksdb::MergeOperator::MergeOperationInputV3 const &,struct rocksdb::MergeOperator::MergeOperationOutputV3 *)const " (?FullMergeV3@MergeOperator@rocksdb@@UEBA_NAEBUMergeOperationInputV3@12@PEAUMergeOperationOutputV3@12@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual bool __cdecl rocksdb::MergeOperator::FullMergeV2(struct rocksdb::MergeOperator::MergeOperationInput const &,struct rocksdb::MergeOperator::MergeOperationOutput *)const " (?FullMergeV2@MergeOperator@rocksdb@@UEBA_NAEBUMergeOperationInput@12@PEAUMergeOperationOutput@12@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > __cdecl rocksdb::Customizable::SerializeOptions(struct rocksdb::ConfigOptions const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &)const " (?SerializeOptions@Customizable@rocksdb@@MEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@AEBUConfigOptions@2@AEBV34@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > __cdecl rocksdb::Customizable::GetOptionName(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &)const " (?GetOptionName@Customizable@rocksdb@@MEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@AEBV34@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual class rocksdb::Status __cdecl rocksdb::Customizable::GetOption(struct rocksdb::ConfigOptions const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > *)const " (?GetOption@Customizable@rocksdb@@UEBA?AVStatus@2@AEBUConfigOptions@2@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@PEAV56@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual bool __cdecl rocksdb::Customizable::AreEquivalent(struct rocksdb::ConfigOptions const &,class rocksdb::Configurable const *,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > *)const " (?AreEquivalent@Customizable@rocksdb@@UEBA_NAEBUConfigOptions@2@PEBVConfigurable@2@PEAV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: __cdecl rocksdb::DBOptions::DBOptions(void)" (??0DBOptions@rocksdb@@QEAA@XZ)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "private: virtual bool __cdecl rocksdb::AssociativeMergeOperator::PartialMerge(class rocksdb::Slice const &,class rocksdb::Slice const &,class rocksdb::Slice const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > *,class rocksdb::Logger *)const " (?PartialMerge@AssociativeMergeOperator@rocksdb@@EEBA_NAEBVSlice@2@00PEAV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@PEAVLogger@2@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "private: virtual bool __cdecl rocksdb::AssociativeMergeOperator::FullMergeV2(struct rocksdb::MergeOperator::MergeOperationInput const &,struct rocksdb::MergeOperator::MergeOperationOutput *)const " (?FullMergeV2@AssociativeMergeOperator@rocksdb@@EEBA_NAEBUMergeOperationInput@MergeOperator@2@PEAUMergeOperationOutput@42@@Z)
IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "bool const rocksdb::kDefaultToAdaptiveMutex" (?kDefaultToAdaptiveMutex@rocksdb@@3_NB)
ifcwrap\_ifcopenshell_wrapper.cp311-win_amd64.pyd : fatal error LNK1120: 34 unresolved externals

Or on Unix:
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcEntityInstanceData.cpp.o): in function `rocksdb::PinnableSlice::PinnableSlice(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*)':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:141: undefined reference to `rocksdb::Cleanable::Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcEntityInstanceData.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()'
/usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::Configurable::~Configurable()':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/configurable.h:59: undefined reference to `vtable for rocksdb::Configurable'
/usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/configurable.h:59: undefined reference to `vtable for rocksdb::Configurable'
/usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/configurable.h:59: undefined reference to `vtable for rocksdb::Configurable'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::Customizable::GetOptionsPtr(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/customizable.h:105: undefined reference to `rocksdb::Configurable::GetOptionsPtr(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const'
/usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/customizable.h:105: undefined reference to `rocksdb::Configurable::GetOptionsPtr(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const'
/usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/customizable.h:105: undefined reference to `rocksdb::Configurable::GetOptionsPtr(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::Options::Options()':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/options.h:1628: undefined reference to `rocksdb::DBOptions::DBOptions()'
/usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/options.h:1628: undefined reference to `rocksdb::ColumnFamilyOptions::ColumnFamilyOptions()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `IfcParse::impl::rocks_db_file_storage::rocks_db_file_storage(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, IfcParse::IfcFile*, bool)':
/home/andrej/ifcopenshell/src/ifcparse/IfcFile.cpp:421: undefined reference to `rocksdb::GetSupportedCompressions()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `init_db':
/home/andrej/ifcopenshell/src/ifcparse/IfcFile.cpp:440: undefined reference to `rocksdb::kDefaultToAdaptiveMutex'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::NewLRUCache(unsigned long, int, bool, double, std::shared_ptr<rocksdb::MemoryAllocator>, bool, rocksdb::CacheMetadataChargePolicy, double)':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/cache.h:282: undefined reference to `rocksdb::LRUCacheOptions::MakeSharedCache() const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `init_db':
/home/andrej/ifcopenshell/src/ifcparse/IfcFile.cpp:445: undefined reference to `rocksdb::NewBlockBasedTableFactory(rocksdb::BlockBasedTableOptions const&)'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::DB::OpenForReadOnly(rocksdb::Options const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, rocksdb::DB**, bool)':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/db.h:243: undefined reference to `rocksdb::DB::OpenForReadOnly(rocksdb::Options const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::unique_ptr<rocksdb::DB, std::default_delete<rocksdb::DB> >*, bool)'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::DB::Open(rocksdb::Options const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, rocksdb::DB**)':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/db.h:187: undefined reference to `rocksdb::DB::Open(rocksdb::Options const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::unique_ptr<rocksdb::DB, std::default_delete<rocksdb::DB> >*)'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb_set_view<unsigned long>::iterator::extract_current_value() const':
/home/andrej/ifcopenshell/src/ifcparse/rocksdb_set_view.h:70: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::PinnableSlice::PinnableSlice(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*)':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:141: undefined reference to `rocksdb::Cleanable::Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb_set_view<unsigned long>::iterator::iterator(rocksdb_set_view<unsigned long>::iterator const&)':
/home/andrej/ifcopenshell/src/ifcparse/rocksdb_set_view.h:103: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/rocksdb_set_view.h:106: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `IfcParse::impl::rocks_db_file_storage::process_deletion_inverse(IfcUtil::IfcBaseClass*)':
/home/andrej/ifcopenshell/src/ifcparse/IfcFile.cpp:526: undefined reference to `rocksdb::WriteBatch::~WriteBatch()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::WriteBatch::WriteBatch(unsigned long, unsigned long)':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/write_batch.h:67: undefined reference to `rocksdb::WriteBatch::WriteBatch(unsigned long, unsigned long, unsigned long, unsigned long)'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::WriteBatch::DeleteRange(rocksdb::Slice const&, rocksdb::Slice const&)':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/write_batch.h:164: undefined reference to `rocksdb::WriteBatch::DeleteRange(rocksdb::ColumnFamilyHandle*, rocksdb::Slice const&, rocksdb::Slice const&)'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `IfcParse::impl::rocks_db_file_storage::process_deletion_inverse(IfcUtil::IfcBaseClass*)':
/home/andrej/ifcopenshell/src/ifcparse/IfcFile.cpp:526: undefined reference to `rocksdb::WriteBatch::~WriteBatch()'
/usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/IfcFile.cpp:547: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/IfcFile.cpp:526: undefined reference to `rocksdb::WriteBatch::~WriteBatch()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTIN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x10): undefined reference to `typeinfo for rocksdb::AssociativeMergeOperator'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x20): undefined reference to `rocksdb::Customizable::GetOption(rocksdb::ConfigOptions const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x28): undefined reference to `rocksdb::Customizable::AreEquivalent(rocksdb::ConfigOptions const&, rocksdb::Configurable const*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x38): undefined reference to `rocksdb::Configurable::PrepareOptions(rocksdb::ConfigOptions const&)'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x40): undefined reference to `rocksdb::Configurable::ValidateOptions(rocksdb::DBOptions const&, rocksdb::ColumnFamilyOptions const&) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x50): undefined reference to `rocksdb::Configurable::ParseStringOptions(rocksdb::ConfigOptions const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x58): undefined reference to `rocksdb::Configurable::ConfigureOptions(rocksdb::ConfigOptions const&, std::unordered_map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > const&, std::unordered_map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >*)'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x60): undefined reference to `rocksdb::Configurable::ParseOption(rocksdb::ConfigOptions const&, rocksdb::OptionTypeInfo const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, void*)'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x68): undefined reference to `rocksdb::Configurable::OptionsAreEqual(rocksdb::ConfigOptions const&, rocksdb::OptionTypeInfo const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, void const*, void const*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x70): undefined reference to `rocksdb::Customizable::SerializeOptions(rocksdb::ConfigOptions const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x78): undefined reference to `rocksdb::Customizable::GetOptionName(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0xb8): undefined reference to `rocksdb::MergeOperator::FullMergeV3(rocksdb::MergeOperator::MergeOperationInputV3 const&, rocksdb::MergeOperator::MergeOperationOutputV3*) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0xc0): undefined reference to `rocksdb::AssociativeMergeOperator::PartialMerge(rocksdb::Slice const&, rocksdb::Slice const&, rocksdb::Slice const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*, rocksdb::Logger*) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0xc8): undefined reference to `rocksdb::MergeOperator::PartialMergeMulti(rocksdb::Slice const&, std::deque<rocksdb::Slice, std::allocator<rocksdb::Slice> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*, rocksdb::Logger*) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, unsigned long, DefaultCodec<unsigned long> >::iterator::operator*() const':
/home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:261: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:263: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, unsigned long, DefaultCodec<unsigned long> >::iterator::operator==(rocksdb_map_adapter<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, unsigned long, DefaultCodec<unsigned long> >::iterator const&) const':
/home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:296: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:296: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::PinnableSlice(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*)':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:141: undefined reference to `rocksdb::Cleanable::Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()'
/usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::PinnableSlice(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*)':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:141: undefined reference to `rocksdb::Cleanable::Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter<unsigned long, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, DefaultCodec<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >::iterator::operator*() const':
/home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:261: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:263: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter<unsigned long, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, DefaultCodec<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >::find(unsigned long const&) const':
/home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:327: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter<std::tuple<int, int, int>, std::vector<unsigned int, std::allocator<unsigned int> >, DefaultCodec<std::vector<unsigned int, std::allocator<unsigned int> > > >::find(std::tuple<int, int, int> const&) const':
/home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:327: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter<std::tuple<int, int, int>, std::vector<unsigned int, std::allocator<unsigned int> >, DefaultCodec<std::vector<unsigned int, std::allocator<unsigned int> > > >::iterator::operator*() const':
/home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:261: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o):/home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:263: more undefined references to `rocksdb::Slice::ToString[abi:cxx11](bool) const' follow
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::PinnableSlice(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*)':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:141: undefined reference to `rocksdb::Cleanable::Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, unsigned long, DefaultCodec<unsigned long> >::find(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const':
/home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:327: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, unsigned long, DefaultCodec<unsigned long> >::iterator::iterator(rocksdb_map_adapter<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, unsigned long, DefaultCodec<unsigned long> >::iterator const&)':
/home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:230: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:233: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::PinnableSlice(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*)':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:141: undefined reference to `rocksdb::Cleanable::Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()':
/home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()'
/usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()'
/usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_set_view<unsigned long>::iterator::operator==(rocksdb_set_view<unsigned long>::iterator const&) const':
/home/andrej/ifcopenshell/src/ifcparse/rocksdb_set_view.h:170: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
/usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/rocksdb_set_view.h:170: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const'
collect2: error: ld returned 1 exit status
make[2]: *** [ifcconvert/CMakeFiles/IfcConvert.dir/build.make:236: ifcconvert/IfcConvert] Error 1
make[1]: *** [CMakeFiles/Makefile2:569: ifcconvert/CMakeFiles/IfcConvert.dir/all] Error 2
2026-02-26 17:12:44 +05:00
Andrej730 6492fdeb05 build-all - add zlib and openssl to RHEL packages 2026-02-26 17:12:44 +05:00
Andrej730 8eb0641e70 build-all - mention zlib requirement 2026-02-26 17:12:44 +05:00
Andrej730 6face696cb build-all - use cmake arg instead of a patch to disable ExpToCasExe 2026-02-26 17:12:42 +05:00
Andrej730 4c4eed5dd4 build_rocky - switch to rocky 9
As rocky 8 is not updating anymore for 2 years and we need some updated dependencies (e.g. `bison` 3.5+ for newer version of `swig`).
2026-02-26 17:01:56 +05:00
Andrej730 634600b65f FindOpenCASCADE - rescan dependencies for cmake config 2026-02-26 17:01:56 +05:00
Andrej730 d43b9ee353 build-all - ensure Python was built with openssl 2026-02-26 17:01:56 +05:00
Andrej730 5ea4290920 build-all - ensure bison is installed 2026-02-26 17:01:56 +05:00
Andrej730 c8ca904333 build-all - distinct command and path in logs 2026-02-26 17:01:56 +05:00
Andrej730 59c28b5ae6 build_rocky - use dnf instead of yum
It's using `dnf` either way, but just to make it more explicit.
2026-02-26 17:01:56 +05:00
Andrej730 c649a4b522 build-all - don't fail silently on missing Python dependencies 2026-02-26 17:01:55 +05:00
Andrej730 5ebd4256a1 build-all-win.py - fix missing compression
Resulting in larger zip files for builds, reported in 7404
2026-02-26 17:01:55 +05:00
Andrej730 3ec9d69556 build-deps - support building Boost for VS2026 2026-02-26 17:01:55 +05:00
Andrej730 54a6fb651e tool.ps1 - support commands with 0 args
No such commands atm though.
2026-02-26 17:01:55 +05:00
Andrej730 dcac336b98 tool.ps1 - refer to cecho.cmd directly, use return instead of exit 0
Which is useful when debugging and calling tools.ps1 directly - less thing to modify to make it work.
Also replaced `exit 0` with `return`, so it would be possible to reuse functions inside `tools.ps1`
2026-02-26 17:01:55 +05:00
Andrej730 88a5717295 build-deps.cmd - fix issue building opencollada in cmake 4 2026-02-26 17:01:55 +05:00
Andrej730 8bfceec1fd vs-cfg.cmd - document some output vars 2026-02-26 17:01:55 +05:00
Andrej730 3807479e42 build-deps - update occt config to support cmake 4
And also to make it work in sync with `build-all.py`.
2026-02-26 17:01:54 +05:00
Andrej730 aebcb676f2 windows - add occt patch to support cmake 4 2026-02-26 17:01:54 +05:00
Andrej730 ba5ea08aee Bump swig version to support cmake 4 2026-02-26 17:01:54 +05:00
Andrej730 d4ebf3f308 cmake - error if svgpp submodule is not initialized 2026-02-26 17:01:54 +05:00
Andrej730 d9e488d518 cmake format 2026-02-26 17:01:54 +05:00
Andrej730 1fb6227d13 build-deps - use other mpir fork to support VS 2026 2026-02-26 17:01:54 +05:00
Andrej730 0e90cd81b3 vs-cfg.cmd - add support for Visual Studio 18 2026 2026-02-26 17:01:53 +05:00
Andrej730 ac23c7a74b vs-cfg.cmd - more readable error on supported versions of VS 2026-02-26 17:01:51 +05:00
Andrej730 7322082a0d typing 2026-02-26 17:01:48 +05:00
Andrej730 0234809d0b Prefer direct api calls over tool.Ifc.run 2026-02-26 17:01:48 +05:00
Andrej730 38381f44b9 ci-bonsai-daily - generate timestamp once for all builds
To avoid running in a situation when some builds are using one tag and some are using another and then unstable repo script fails to find builds for some platforms.
2026-02-26 17:01:47 +05:00
Andrej730 83fed6257e run-cmake.bat - deduplicate cmake args code 2026-02-26 17:01:47 +05:00
Andrej730 40d43732ca build-deps - bump proj version to avoid errors in cmake 4+ 2026-02-26 17:01:47 +05:00
Andrej730 521c8eae0d run-cmake.bat - document USE_NINJA env var 2026-02-26 17:01:47 +05:00
Andrej730 5aa7ba6be6 IfcConvert - fix Windows builds stuck on 0.8.0 version 2026-02-26 17:01:47 +05:00
Thomas Krijnen e3464b395e --recursion-limit option in validate.py 2026-02-26 12:38:36 +01:00
Thomas Krijnen 9ab9da2ca8 Update black exclude dirs 2026-02-26 12:36:17 +01:00
Thomas Krijnen 077a0c3755 Run black on express/ 2026-02-26 12:36:01 +01:00
Thomas Krijnen 18527a78e1 rule_executor.py don't log RecursionError as error 2026-02-26 12:35:40 +01:00
falken10vdl 0be348707c Update scale_font_size method to accept a None parameter so it is cleaner the calls from the rest of the code base 2026-02-25 13:28:58 -03:00
falken10vdl 1b784e22af Add decorator font scale property addon setting 2026-02-25 13:28:58 -03:00
falken10vdl c0857c715a Refactor scale_font_size to improve DPI and pixel size handling for better font scaling 2026-02-25 13:28:58 -03:00
falken10vdl ecaea5f776 refactor scale_font_size as per developers feedback 2026-02-25 13:28:58 -03:00
falken10vdl dde6e2d62b black 2026-02-25 13:28:58 -03:00
falken10vdl 8df4b2cd56 Scale font size in PolylineDecorator and BoundingBoxDecorator based on Blender's UI preferences 2026-02-25 13:28:58 -03:00
Richard Brice ece7d6b97f Fixes example in documentation 2026-02-25 14:21:10 +01:00
Thomas Krijnen 2d7a556dd5 Fix sectioned solid cap #7674 2026-02-24 11:26:18 +01:00
Dion Moult dcc25038f9 Fix #7646. Bug with layer thumbnail orientation. 2026-02-24 10:03:33 +11:00
Dion Moult 0d382119dd Fix docs table for selector syntax 2026-02-24 09:53:57 +11:00
Dion Moult 41afaaec0d Revert "Fix #7681: Fix isolate_objects ignoring hide_select/hide_viewport (#7710)"
This reverts commit 7d8c7a2c3d.
2026-02-24 09:53:28 +11:00
Dion Moult f69ea82789 Revert "Fix #7646: Fix layer thumbnail orientation for IFC types"
This reverts commit 514cbb49cc.
2026-02-24 09:53:27 +11:00
Dion Moult 2f5c71588e Revert "Table was not rendering correctly."
This reverts commit 9ff4a7f0e0.
2026-02-24 09:53:24 +11:00
Ryan Schultz 7d8c7a2c3d Fix #7681: Fix isolate_objects ignoring hide_select/hide_viewport (#7710)
Objects with hide_select=True could not be selected during
isolation, causing hide_view_set to incorrectly hide them.
Objects with hide_viewport=True had their H-key hide state
modified as a side effect of hide_view_clear/hide_view_set.
Both are now left unaffected by bim.activate_drawing.

Generated with the assistance of an AI coding tool.
2026-02-23 07:09:18 -06:00
Ryan Schultz 514cbb49cc Fix #7646: Fix layer thumbnail orientation for IFC types
Use EPset_Parametric.LayerSetDirection exclusively to
determine horizontal vs vertical layer rendering in type
thumbnails, rather than hardcoding IfcSlabType checks.
Also fix line drawing to use the is_horizontal flag
consistently.

Generated with the assistance of an AI coding tool.
2026-02-22 17:46:10 -06:00
Ryan Schultz 9ff4a7f0e0 Table was not rendering correctly.
Fix Sphinx docs: replace csv-table with list-table for formatting functions

The documentation table of formatting/query functions was not rendering
because `.. csv-table::` requires strict RFC4180 CSV escaping. The table
contains nested quotes, inch marks (e.g. `3' - 0"`), backticks, and code
examples, which cause the CSV parser in docutils to treat rows as malformed
and drop the entire directive.

Replaced the directive with `.. list-table::`, which parses reStructuredText
instead of CSV and safely supports inline code, quotes, and multi-line cells.

Also moved the examples text outside the directive block and ensured a blank
line after the table so Sphinx does not interpret following paragraphs as
table rows.

No content changes — documentation now renders correctly.

Generated with the assistance of an AI coding tool.
2026-02-22 15:10:29 -06:00
dependabot[bot] 8afe05601e Bump tar from 7.5.7 to 7.5.9 in /src/ifctester/webapp
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.7 to 7.5.9.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.7...v7.5.9)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-20 08:56:50 +11:00
falken10vdl 1751c36c67 AddReferenceImage: implement option to show texture in solid mode (#7689) 2026-02-19 21:02:57 +11:00
falken10vdl d4388ec76d AddReferenceImage: fix regression with IFC2X3 support, refactor to no longer depend on add_representation or update_representation, remove legacy style updating functionality
* Enhance AddReferenceImage operator to use file browser instead of independent popup dialogue

* Fix dimensions assertion in TestAddReferenceImage

* Remove error in return in _execute (it is not execute)

* Add  IFC2X3 support to AddReferenceImage

* Adde unit="LENGTH" to the x/y properties (every length dimension everywhere in the UI is in project length units. No need to say it explicitly)

* Manually create the texture always, not just for IFC2X3

* Add poll method to AddReferenceImage operator to check for loaded IFC project

* Refactor AddReferenceImage to add representation manually following pattern in root/operator.py's bim.add_element

* Improve File explorer options between new and select from existing project Ifc Reference Images

* Refactor get_existing_reference_images to use selector for filtering image annotations

* No extra args needed after should_add_representation is False

* Doing clean=True deletes everything

* Don't manually add geometry and materials, don't call bpy.ops. Only create IFC data, then use preexisting loading functions to create geometry.

* Black formatting, also now we can start to remove this operator as it becomes obsolete

* Consolidate duplicate UV generation into Loader.load_generated_uv_map

Replace 3 identical XY-UV baking blocks (create_object IMAGE,
bm_add_image_plane, ImageScalingTool) with a single reusable
classmethod in tool.Loader.

* Fix IFC4 texture display in Solid viewport Texture mode

IFC4 IfcTextureCoordinateGenerator Mode=COORD is used, load_texture_maps
falls back to load_generated_uv_map to bake XY-UV data onto the mesh.

* Fix IFC2X3 texture display

* This looks wrong

* Remove legacy override image feature, because we now have a proper styles and texture manager

* Remove legacy override existing image element, because we now have a dedicated styles texture manager

* Remove unnecessary roundtrip to bmesh and mesh

---------

Co-authored-by: Dion Moult <dion@thinkmoult.com>
2026-02-19 11:56:10 +11:00
falken10vdl 7141f2cf90 Fixes to PR7607 (Linked IFC Projects): Wireframe toggle. More permisive to get has_transformation = False. Show enable_editing_link if link is loaded 2026-02-19 10:53:01 +11:00
Sebastian Schilling 418d410b5c moved change of bsdd baseurl change to addon settings 2026-02-18 09:11:25 +11:00
Sebastian Schilling a5461c0748 buildingSMART Data Dictionary module: added textfield to change data dictionary url 2026-02-18 09:11:25 +11:00
Bruno Postle 291e815770 Add AGENTS.md contributor guide
Guidelines for external contributors using AI coding tools,
covering licensing, AI disclosure requirements, PR scope,
commit style, code formatting, and testing expectations.

Generated with the assistance of an AI coding tool.
2026-02-18 09:09:46 +11:00
Dion Moult ed500d58ba For consistency, maxfail=1 for module tool tests 2026-02-17 18:16:33 +11:00
Dion Moult 8023a992da Fix tests where panel name and tab panel name is identical
For now probably just easier to skip tabs. They are just containers and
not worth testing. Famous last words :)
2026-02-17 18:16:20 +11:00
Dion Moult fcc80ad14a Simplify add reference image size implementation and fix segfaulting tests
Previously, there was a dance between invoke, execute, and draw. This
can probably be resolved, but is a high-risk for undo bugs. This
simplifies the logic flow to just a traditional _invoke -> _execute.

I add a new feature test to at least make sure it does something, and
this also fixes the segfault in tool tests as it no longer requires the
launching of the file browser.
2026-02-17 18:11:13 +11:00
José Aliste c6b14d1474 Fixes snap angle.
In my previous commit, I mistakenly believed that there was an API change from
snap_angle_increment to snap_angle_increment_3d
But since the feature was introduced in blender 4.2 the setting is called
snap_angle_increment_3d.
2026-02-17 13:39:03 +11:00
Dion Moult 37fe0ad993 Reimplement adding multiple references / schedules cf5ffad9af
Previously it was implemented inline. This now implements it as a
tool.Blender function with tests. Also the previous tests didn't
actually run and weren't actually testing any tools despite being in a
tool tests.
2026-02-17 11:18:24 +11:00
Dion Moult e20e286168 Revert "feat(drawing): support multiple file selection in Add Reference"
This reverts commit cf5ffad9af.
2026-02-16 18:25:54 +11:00
Dion Moult 8c0bed0c61 Stub open command so running tests doesn't keep on launching apps 2026-02-16 18:24:37 +11:00
Dion Moult 8cfb162851 Fix #7656. Regression in text editing where leaders were accidentally removed. Added tests. 2026-02-16 17:57:09 +11:00
dependabot[bot] 379c74b31f Bump ruff from 0.15.0 to 0.15.1
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.0 to 0.15.1.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.0...0.15.1)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 14:57:13 +11:00
Dion Moult 1d1f158fe2 Remove no longer relevant invoke code for linking IFCs 2026-02-16 08:23:35 +11:00
Richard Brice 65d5df7801 IfcAxis2PlacementLinear mapping used with IfcSectionedSurface and IfcSectionedSolidHorizontal
IfcSectionedSurface and IfcSectionedSolidHorizontal both of CrossSectionPositions attributes which are lists of IfcAxis2PlacementLinear. The implementation of each class used its own bespoke mapping of IfcAxis2PlacementLinear, which were identical to each other and slightly different than IfcAxis2PlacementLinear. Now the two sectioned classes use the one and only mapping for IfcAxis2PlacementLinear
2026-02-15 11:16:07 -08:00
falken10vdl b246998f68 Linked IFC projects enhancement (multiple links to same project file) (#7607)
* Linked IFC projects enhancement (multiple links to same project file)

- Implement link management system using UUIDs as identifiers to support multiple links to the same IFC file
- Add georeferencing compatibility detection and UI display (NONE, NOT_COMPATIBLE, PARTIAL_COMPATIBLE, FULL_COMPATIBLE)
- Support for duplicate link creation with Shift+D shortcut and automatic position offset
- Add false origin and project north calculation from 3D cursor for MANUAL mode
- Only store one cache per file, regardless of the amount of links
- Prevent duplicate links based on filepath and position comparison
- Improve error handling for missing files and loading failures
- Update tests

* Remove duplicate georef UI

I try to avoid duplicate UI (especially for one that can be as
sophisticated as georef - e.g. missing is WCS) as it means double the
code, double the tests, potential user confusion. BTW the note about
vertical datum isn't quite accurate as it may be included in the CRS
definition so vertical datum is optional.

* Remove depsgraph_update_post handler for update_link_ui_on_transform as per core developer feedback

* Move get_projected_crs to geolocation module

* Refactor get_projected_crs to simplify as per core developer feedback

* Remove unused import of bonsai.tool from project module

* Use IfcDocumentInformation per linked file and IfcDocumentReference for locaiton information

* Refactor SaveBlendMetadataFile operator to remove  try-except blocks and remove linked projects collections since they are recreated by bonsai

* Cleanup removing empty collection instances for linked models in metadata.blend file and call determine_georeferencing_compatibility on link reload

* Add locking mechanism for linked models and update UI to reflect lock status

* Update logic that track IFC to execute_ifc_duplicate_operator instead of having it in execute() which does not track IFC undo/redo

* Refactor link handling to use get_link_empty_handle and set_link_empty_handle methods which in turn use the standard blender-ifc integrations patters (tool.Ifc.get_object(doc_reference) and tool.Ifc.link(doc_reference, empty_handle)

* remove operator.DuplicateLink and move it to tool.Project.duplicate_link()

* Refactor link handling to use sequential identifiers (no need for STEP ID DocRef)

* Refactor IFC linking logic to handle cases without a parent IFC file loaded. Firts link flase origin becomes parent origin

* Lock should not affect selection.

This makes it consistent with grid / spatial lock, and also toggle
selectability is already implemented.

* Remove unnecessary check for loaded library as Blender seems to do this internally already

* Rename util to get_crs because in IFC4X3 you can also have geographic CRS not just projected

* Remove unnecessary call to determine_georeferencing_compatibility

This function is already always called prior to calculate_link_position
so shouldn't be called here. It's also a very expensive function: as it
currently stands, just to link a single IFC, ifcopenshell.open() is
called 3 times. This reduces it to 2.

* Store CRS as metadata for linked models, and compare metadata when indicating georeferencing compatibility

Previously, to check georeferencing compatibility, ifcopenshell.open()
was used. When linking large models, this adds considerable time and
memory usage. This instead captures the georef as standard metadata in
our .cache.json. This now reduces the ifcopenshell.open() calls back
down to only 1 as necessary (see previous commit).

* Use link index instead of link name to fetch link collection item

Link name runs into issues with name uniqueness. This is why you created
a function for "get next link ID". After this refactoring, we can no
longer worry about uniqueness and that function may be removed.

* Simplify reloadlink into just unload and reload (with cache disabled)

This function should not be responsible for editing any data.

* Remove unnecessary get_next_link_id as names no longer need uniqueness

This now frees up the name variable to track a more meaningful, human
name like IfcDocumentInformation's Name attribute.

* Rewrite get / set link_empty_handle to just use the link directly

This prevents needless logic to fetch the link and also removes issues
related to duplicate names.

* Temporarily remove logic in prop callback

Right now, pretty much all the logic is done in a prop callback. In
general logic in prop callbacks should be minimised, since it's hard to
test and easily triggered as a domino effect of another change, and may
also impact undo/redo.

* Remove code that unnecessarily removes cache

This code removes cache, which means any project unlinking an IFC auto
clears the cache for any other project which doesn't make sense, and
also breaks the ability to readd it quickly.

* Rewrite link, unlink, load, and unload IFC

There were a few issues tackled here:

 - Operators that change any IFC data must use tool.Ifc.Operator and
_execute, otherwise undo/redo will break. That's one of the risks of
using prop callbacks, as it is not explicit when an IFC edit happens.
 - The usage of IfcDocumentReference was not correct. The Location
should store the URL, _not_ the position. The position should be in the
Identification attribute.
 - The URL was stored in IfcDocumentInformation location, which does not
work in IFC2X3. There are a few changes here to make it IFC2X3
compatible.
 - Generally move logic in operators, not prop callback.

* Remove restriction around manual mode.

Users should be able to use manual mode if they want.

* Restore AUTOMATIC mode to identical behaviour to file open

This is the first step to reusing cache files agnostic of the host.

* Revert tests for a fresh start for updating tests

* Revert "test_feature - clean up .ifc.cache. files after test was executed"

This reverts commit 99ae768ddf.

* Update tests and reimplement calculations for matrix of empty handle

Previously, the empty would always be placed at the origin, unless a
"position" offset was present. This is a problem, because the "position"
is simply a local offset relative to the Blender cache! If the cache was
regenerated, the offsets would be outdated. Also, the cache appeared in
different locations depending on the false origin mode, so the offset
would mean different things to different people.

Instead, a more robust method is:

 1. When you link a file, a Blender cache is generated. The Blender
origin of this cache is arbitrary! It depends on the user's false origin
mode and is purely a Blender session specific thing.
 2. When you load a link, a link is _always_ loaded into the correct
location with regards to IFC global coordinates. All math is done from
the perspective of IFC.
 3. If you choose to transform (move / rotate / scale!?) this link from
its correct location, that gets recorded as a 4x4 transformation matrix.
Note: I haven't implemented this properly yet.

Tests all pass, with a minor modification to the new behaviour that
false origin mode now won't affect the location it ends up in, only the
generation of the cache.

* Remove arbitrary convention around display name

Not needed anymore now that A/M/D is a detail and not significant on
actual coordinates, and also that the UUID is no longer needed.

* Simplify implementation of loading linked models when opening an IFC

* Move link matrix calculation from operator to tool for reuse

* Implement editing link location and calculation of transformation matrix

I changed my mind on the is_locked thing, since it isn't clear to the
user that locking need to be done to save changes.

* Remove old is_locked, prop update callback no longer needed (dedicated operator instead), remove old calculation code

* Simplify code related to placed_as_per_georef

* For now, simple skip for duplicate / delete

IMO duplicate / delete / move a link are very rare and explicit
operations.

* Update tests

* Remove host_model coordinate data as cache is no longer host model dependent

* Move icons outside list because there are too many

* Minor tweaks

---------

Co-authored-by: Dion Moult <dionmoult@gmail.com>
Co-authored-by: Dion Moult <dion@thinkmoult.com>
2026-02-15 19:28:43 +11:00
Ryan Schultz a88c5938dc typos 2026-02-14 12:18:29 -06:00
Thomas Krijnen 7978f1fb08 Fix compilation on gcc #7666 2026-02-13 10:17:28 +01:00
ssg3d 7b4889d2ec Update IfcParse.cpp
IfcOpenshell read file, and write file without changes. This round trip introduces truncation noise. It should not hurt to increase the precision to keep this clean.
2026-02-13 10:03:05 +01:00
José Aliste 740fcf7768 Use Blender's angle snap setting in wall.py and profile.py
Replace hardcoded 5-degree angle snapping with Blender's
snap_angle_increment setting in create_wall_from_2_points()
and create_profile_from_2_points().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 22:29:43 -03:00
José Aliste 067e04b564 Use Blender's angle snap setting in model/polyline.py
Replace hardcoded 5-degree angle snapping with Blender's
snap_angle_increment setting in handle_lock_axis() for:
- Initial angle rounding when locking axis (A key)
- Angle rounding and increments on Shift+Wheel scroll

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 22:29:43 -03:00
José Aliste cd95f46db5 Use Blender's angle snap setting in tool/polyline.py
Replace hardcoded 5-degree angle snapping with Blender's
snap_angle_increment setting in calculate_distance_and_angle().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 22:29:43 -03:00
José Aliste 348e48b49c Add get_angle_snap_value() helper to tool/snap.py
This function retrieves the angle snap increment from Blender's
tool_settings.snap_angle_increment property, which was added in
Blender 4.2. This allows users to configure the angle snap value
through Blender's native UI instead of using hardcoded values.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 22:29:43 -03:00
Thomas Krijnen 6bb8abdc5a Fix for 4.2 schema after a46cdbb907 2026-02-11 12:11:23 +01:00
Thomas Krijnen b7a8c9b330 Fix for 4.2 schema after a46cdbb907 2026-02-11 11:43:58 +01:00
Thomas Krijnen e6780973da arrange_poly: Refactor into logical blocks; add timing 2026-02-10 14:01:10 +01:00
Thomas Krijnen c6072e416c N-Section Lofting for Non-Polygonal (Curved) Shapes #7658 2026-02-10 11:04:43 +01:00
Thomas Krijnen f5686fff26 Simplify destructor by removing null check #7650 2026-02-10 09:25:02 +01:00
Thomas Krijnen a46cdbb907 Ignore site placement also for site geometry - add queue #7654 2026-02-09 21:37:21 +01:00
Thomas Krijnen a4d5d4e19a Ignore site placement also for site geometry #7654 2026-02-09 21:21:10 +01:00
Thomas Krijnen 4ec643d4e1 schema entity initialize() method, optional argument forwarding in file::create, populate_derived in InstanceData 2026-01-15 19:24:55 +01:00
Thomas Krijnen c089c11cd6 Hack a bit to make stream tests run 2026-01-15 16:22:18 +01:00
Thomas Krijnen 8afba9a665 Hack a bit to make sql tests run 2026-01-15 16:12:25 +01:00
Thomas Krijnen 5c4046e054 declaration as property 2026-01-15 16:11:58 +01:00
Thomas Krijnen d82e1da907 Don't leak parent id into type decl instances 2026-01-15 15:38:49 +01:00
Thomas Krijnen 8c2e1226c9 declaration property 2026-01-15 15:38:32 +01:00
Thomas Krijnen ed8821486d Broaden __eq__ for type decl instances 2026-01-15 14:23:49 +01:00
Thomas Krijnen 742bfac144 _remove, schema_identifier and test_file 2026-01-15 14:13:29 +01:00
Thomas Krijnen 1f4c4204d0 Re-enable setting logical with UNKNOWN in python 2026-01-15 13:13:51 +01:00
Thomas Krijnen 43f78605f2 Small tweak to invocation of test/test_rules.py 2026-01-15 13:00:48 +01:00
Thomas Krijnen 6cd1375cf9 Rerun rule compilation for exists() on indeterminate 2026-01-15 13:00:37 +01:00
Thomas Krijnen 2d1250c18f Global file for rule and derived attributes 2026-01-15 12:39:32 +01:00
Thomas Krijnen bee61cba11 Accept exact schema_identifier in file() 2026-01-15 12:38:36 +01:00
Thomas Krijnen 33f072e0a7 Account for removed instance factory 2026-01-15 12:37:53 +01:00
Thomas Krijnen 490fa92dc0 Rework equality and get_info_2 2026-01-15 12:37:12 +01:00
Thomas Krijnen cf3393b6a0 black 2026-01-14 14:09:40 +01:00
Thomas Krijnen fbbc92c5d7 Hashing solely based on identity 2026-01-14 14:03:55 +01:00
Thomas Krijnen 574827016a Initialization of header and file 2026-01-14 14:03:28 +01:00
Thomas Krijnen 18c9140700 Data types 2026-01-14 14:03:03 +01:00
Thomas Krijnen 9e165319e6 Fix schema passing to file creation 2026-01-14 14:02:48 +01:00
Thomas Krijnen 64bf807baa Fix add entity with id 2026-01-14 14:02:10 +01:00
Thomas Krijnen 136befde86 Fix write() call 2026-01-14 14:01:39 +01:00
Thomas Krijnen 1a4d750ecd Consistency of get_inverse calls 2026-01-14 14:00:50 +01:00
Thomas Krijnen 0a5dd78774 Fix add entity with id 2026-01-14 14:00:00 +01:00
Thomas Krijnen 94427ebcb3 Allow setting of history and future 2026-01-14 13:59:26 +01:00
Thomas Krijnen 9147938c36 Aggregate data types 2026-01-14 13:59:07 +01:00
Thomas Krijnen ffcc02fb99 Remove global create_entity() call 2026-01-14 13:57:26 +01:00
Thomas Krijnen e4e4f31d20 Defer deletion so that traversal still works 2026-01-13 08:45:04 +01:00
Thomas Krijnen 2dd002bcd8 Properly constuct type decl instances 2026-01-13 08:44:44 +01:00
Thomas Krijnen 7e5248da29 Fix create_shape() overloads because SWIG does not map None for us anymore 2026-01-13 08:44:30 +01:00
Thomas Krijnen 07a01bcf54 Process derived attributes that are not redeclared (C++ has no knowledge of them) 2026-01-13 08:43:52 +01:00
Thomas Krijnen 0a9e29ce45 black 2026-01-10 11:52:08 +01:00
Thomas Krijnen 0604db06e9 typename 2026-01-10 11:52:05 +01:00
Thomas Krijnen 991556fbe3 template as 2026-01-10 11:03:26 +01:00
Thomas Krijnen b66b04b001 Remove usage of .wrapped_item and some other fixes 2026-01-10 11:01:18 +01:00
Thomas Krijnen 5c9213426f Hacks and fixes to get python code back in reasonable state 2026-01-10 10:21:09 +01:00
Thomas Krijnen 69a4ad35a1 Remaining cpp changes 2026-01-10 10:20:34 +01:00
Thomas Krijnen 7f4d9e31c3 Add test about deletion 2026-01-08 11:52:04 +01:00
Thomas Krijnen ae79996eb6 Fix running of test/tests.py 2026-01-08 11:49:29 +01:00
Thomas Krijnen f5b2358c2e Make Base::data() private, file::add(..., id) 2026-01-08 10:35:46 +01:00
Thomas Krijnen f9c791de1b Fix examples 2026-01-08 09:44:43 +01:00
Thomas Krijnen 5b85a2c38b Fix examples 2026-01-08 09:44:00 +01:00
Thomas Krijnen bc46a8f85b Fixes 2026-01-08 09:38:46 +01:00
Thomas Krijnen c57209fbce Fixes 2026-01-07 20:37:10 +01:00
Thomas Krijnen 088a6b7204 Reinstate IfcAlignment example 2026-01-07 18:15:23 +01:00
Thomas Krijnen f2f4d626a5 Fix examples mostly 2026-01-07 13:51:48 +01:00
Thomas Krijnen 572a5655cf Fixes for gcc 2026-01-06 13:23:48 +01:00
Thomas Krijnen ccd91c0ff0 No more messing around with specific sfinae as<>() in Select implementations 2026-01-06 11:27:29 +01:00
Thomas Krijnen a1d2c902f3 Add enum to forward declarations for Ifc2x3::IfcNullStyle 2026-01-06 09:56:36 +01:00
Thomas Krijnen 3ae361bb3b Rerun codegen 2026-01-06 09:49:28 +01:00
Thomas Krijnen 2d7521ed88 Move template down to .cpp to prevent use of incomplete type 2026-01-06 09:41:20 +01:00
Thomas Krijnen 7098beb819 Work towards v1.0 data model with encapsulated weak_ptr as basis for instances 2026-01-05 21:42:01 +01:00
Thomas Krijnen f09ca658f1 Initial investigation into lofting open profile with tags 2025-12-02 16:34:24 +01:00
1852 changed files with 561427 additions and 380973 deletions
+3 -2
View File
@@ -1,7 +1,8 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/BlankSpruce/gersemi/0.24.0/gersemi/configuration.schema.json
# Needed for gersemi to detect custom functions and macros.
definitions: ["./cmake"]
# Gersemi doesn't support autodetection of macros/functions from other files or from the current one
# and requires to explicitly list directories/cmake files that define them.
definitions: ["./cmake", "./src"]
disable_formatting: false
extensions: []
indent: 4
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env -S uv run
# /// script
# dependencies = [
# "PyGithub",
# "requests",
# ]
# ///
import os
from pathlib import Path
import requests
from github import Github
from github.GitReleaseAsset import GitReleaseAsset
EXTENSION_ID = "bonsai"
CURRENT_PYTHON_VERSION = "py313"
CURRENT_PLATFORMS = ["linux-x64", "macos-arm64", "windows-x64"]
def publish_asset(asset: GitReleaseAsset, token: str, repo_root: Path) -> None:
"""
Publish an asset to Blender Extensions.
Reference: https://extensions.blender.org/api/v1/swagger
"""
temp_path = repo_root / asset.name
response = requests.get(asset.browser_download_url)
response.raise_for_status()
temp_path.write_bytes(response.content)
url = f"https://extensions.blender.org/api/v1/extensions/{EXTENSION_ID}/versions/upload/"
headers = {"Authorization": f"Bearer {token}"}
files = {"version_file": temp_path.read_bytes()}
response = requests.post(url, headers=headers, files=files)
response.raise_for_status()
temp_path.unlink()
print(f"✓ Published {asset.name}")
def main() -> None:
token = os.getenv("BLENDER_EXTENSIONS_TOKEN")
if not token:
raise Exception("BLENDER_EXTENSIONS_TOKEN environment variable not set")
# Get the repository root
repo_root = Path(__file__).parent.parent.parent
# Read VERSION file
version_file = repo_root / "VERSION"
version = version_file.read_text().strip()
print(f"Current VERSION: {version}")
tag_name = f"bonsai-{version}"
# Get release from GitHub
gh = Github()
gh_repo = gh.get_repo("IfcOpenShell/IfcOpenShell")
release = gh_repo.get_release(tag_name)
assets = release.get_assets()
asset_platform_map: dict[str, tuple[GitReleaseAsset, str]] = {}
for asset in assets:
if CURRENT_PYTHON_VERSION not in asset.name:
continue
for platform in CURRENT_PLATFORMS:
if platform in asset.name:
asset_platform_map[asset.name] = (asset, platform)
break
if len(asset_platform_map) != len(CURRENT_PLATFORMS):
found_platforms = {platform for _, (_, platform) in asset_platform_map.items()}
missing_platforms = set(CURRENT_PLATFORMS) - found_platforms
raise Exception(
f"Expected {len(CURRENT_PLATFORMS)} assets but found {len(asset_platform_map)}. "
f"Missing: {', '.join(sorted(missing_platforms))}"
)
print("\nRelease assets:")
for asset_name in sorted(asset_platform_map.keys()):
print(f"- {asset_name}")
# https://extensions.blender.org/api/v1/swagger
print("\nPublishing assets to Blender Extensions:")
for asset_name, (asset, platform) in asset_platform_map.items():
publish_asset(asset, token, repo_root)
if __name__ == "__main__":
main()
@@ -0,0 +1,46 @@
# Lint/test gate for the bonsaiviewer-autodesk crate — nothing here ships.
# The connector binary that reaches users is built by the platform pipelines
# (build_rocky.yml, build_rocky_arm.yml, build_win.yml, build_osx.yml), each
# of which runs packaging/build.py itself and bundles dist/autodesk/ into the
# Bonsai Viewer archive.
name: Test Bonsai Viewer Autodesk Connector
on:
workflow_dispatch:
push:
paths:
- 'src/bonsaiviewer-autodesk/**'
- '.github/workflows/build-bonsaiviewer-autodesk.yml'
pull_request:
paths:
- 'src/bonsaiviewer-autodesk/**'
- '.github/workflows/build-bonsaiviewer-autodesk.yml'
jobs:
test:
name: cargo-test
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/bonsaiviewer-autodesk
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
# cargo-target reuse across runs. Massive cold-build speedup,
# cheap on the GitHub Actions cache budget.
- uses: Swatinem/rust-cache@v2
with:
workspaces: src/bonsaiviewer-autodesk
- name: cargo fmt --check
run: cargo fmt --all -- --check
- name: cargo clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: cargo test
run: cargo test --all-features
+91 -22
View File
@@ -10,10 +10,11 @@ jobs:
fail-fast: false
matrix:
include:
- os: macos
runner: macos-14
arch: x64
oldarch:
# x64 (Intel cross-compile) dropped while wgpu Qt is required:
# the runner is arm64 so `brew --prefix qt` returns the arm64
# prefix; we'd need a separate x86_64 Qt install under
# /usr/local to cross-build BonsaiViewer. Revisit if Intel-Mac
# demand resurfaces.
- os: macos
runner: macos-14
arch: arm64
@@ -21,12 +22,12 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/build-outputs
path: ./build
@@ -39,7 +40,19 @@ jobs:
brew update
# preinstalled: xz, cmake
brew install git bison autoconf automake libffi findutils
# qt brings in Qt6 + Svg; nix/build-all.py honours pre-set
# QT_DIR so BonsaiViewer doesn't try to aqtinstall (which is
# Linux-only).
brew install qt
echo "$(brew --prefix findutils)/libexec/gnubin" >> $GITHUB_PATH
# Mac is using bison 2.5 by default, but we need 3.5+ for swig.
echo "$(brew --prefix bison)/bin" >> $GITHUB_PATH
# The bonsaiviewer-autodesk connector is a Rust crate; the "Package
# .zip archives" step below runs `cargo build --release` via
# packaging/build.py. Match the dedicated connector workflow's stable
# toolchain, rather than whatever Rust the runner image happens to ship.
- uses: dtolnay/rust-toolchain@stable
- name: Install aws cli
run: |
@@ -47,11 +60,11 @@ jobs:
- name: Unpack Dependencies
run: |
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
cd build
python ../nix/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
uses: hendrikmuhs/ccache-action@v1.2.23
with:
key: mac-${{ matrix.arch }}
@@ -75,13 +88,26 @@ jobs:
/usr/local/bin/brew install gettext openssl
fi
set -o pipefail
export QT_DIR="$(brew --prefix qt)"
# --shared mirrors build_rocky.yml after 27249770e: builds
# IfcOpenShell as shared libs so each plug-in dylib references
# libIfcParse / libIfcGeom via @rpath instead of statically
# embedding them — the dominant size win for BonsaiViewer.app
# (per-plugin libs go from ~30-50 MB to a few MB).
#
# IfcOpenShell-Python is back on after the ifcwrap rpath fix:
# INSTALL_RPATH "$ORIGIN" is a Linux-ism that macOS dyld bakes
# in as a literal string, so `@rpath/ifcopenshell.document.rdb
# .dylib` failed to resolve at import time. ifcwrap now sets
# INSTALL_RPATH to "@loader_path" on Apple.
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release \
python3 ./nix/build-all.py -v --diskcleanup ${MAC_INTEL} \
BUILD_BONSAIVIEWER=ON QT_DIR="${QT_DIR}" \
python3 ./nix/build-all.py -v --diskcleanup --shared ${MAC_INTEL} \
| tee build.log
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: build-logs-osx-${{ matrix.arch }}
path: |
@@ -93,9 +119,7 @@ jobs:
- name: Pack Dependencies
run: |
cd build
for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do
test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir");
done
python ../nix/cache_dependencies.py pack
- name: Commit and Push Changes to Build Repository
run: |
@@ -109,8 +133,28 @@ jobs:
- name: Package .zip archives
run: |
VERSION=v`cat VERSION`
# packaging/build.py stages the connector binary + connector.json
# into dist/autodesk/; the .app loop below copies that folder into
# the bundle. Same on-disk shape as the Linux and Windows builds.
python3 src/bonsaiviewer-autodesk/packaging/build.py
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
test -d "$autodesk_connector_dir"
cd ./build/`uname`/*/10.15/install/ifcopenshell
mkdir ~/output
mkdir -p ~/output
install_root="$PWD"
stage_runtime_payload() {
dest="$1"
while IFS= read -r runtime_file; do
cp -L "$runtime_file" "$dest/"
done < <(
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
[ -d "$runtime_dir" ] || continue
find "$runtime_dir" -type f \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
done
)
}
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
@@ -125,18 +169,43 @@ jobs:
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip ifcopenshell/*
stage_runtime_payload ifcopenshell
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip ifcopenshell
mv *.zip ~/output
popd > /dev/null
done
cd bin
rm *.zip || true
ls | while read exe; do
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip $exe
rm -f "$install_root"/bin/*.zip
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
exe=`basename "$exe_path"`
package_dir="$install_root/.package-${exe}"
rm -rf "$package_dir"
mkdir -p "$package_dir"
cp "$exe_path" "$package_dir/"
stage_runtime_payload "$package_dir"
pushd "$package_dir" > /dev/null
zip -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip" .
popd > /dev/null
rm -rf "$package_dir"
done
# .app bundles (e.g. BonsaiViewer.app) live at the install-prefix
# root because their install rule uses `BUNDLE DESTINATION "."` —
# that's the layout Qt's macdeployqt expects. macdeployqt has
# already embedded the Qt frameworks inside each bundle during
# install/strip, so the only thing left to stage is the connector.
find "$install_root" -maxdepth 1 -type d -name "*.app" | while read app_path; do
app=`basename "$app_path" .app`
if [ "$app" = "BonsaiViewer" ]; then
# ConnectorDiscovery looks in applicationDirPath()/connectors,
# which for a bundle is Contents/MacOS.
mkdir -p "$app_path/Contents/MacOS/connectors"
cp -a "$autodesk_connector_dir" "$app_path/Contents/MacOS/connectors/"
fi
pushd "$install_root" > /dev/null
zip -qq -r "$HOME/output/${app}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip" "$(basename "$app_path")"
popd > /dev/null
done
mv *.zip ~/output
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
+19 -6
View File
@@ -9,13 +9,13 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
path: IfcOpenShell
- name: Checkout Build Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/build-outputs
path: ifcopenshell_build
@@ -26,10 +26,10 @@ jobs:
- name: Unpack Dependencies
run: |
cd ifcopenshell_build
python ../IfcOpenShell/pyodide/cache_dependencies.py unpack
python ../IfcOpenShell/nix/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
uses: hendrikmuhs/ccache-action@v1.2.23
with:
key: ubuntu-22.04-${{ runner.arch }}
@@ -40,9 +40,21 @@ jobs:
NEW_FILE=`echo $FILE | sed "s/-/+${GITHUB_SHA:0:7}-/2"`
mv $FILE $NEW_FILE
- name: Order wheel shared objects
run: |
python ./IfcOpenShell/pyodide/order_pyodide_wheel_shared_objects.py dist/ifcopenshell-*.whl
- name: Split packages
run: |
VERSION=v`cat ./IfcOpenShell/VERSION`
mkdir -p dist-modular
python ./IfcOpenShell/pyodide/split_pyodide_ifcopenshell_wheel.py dist/ifcopenshell-*.whl ./dist-modular
cd dist-modular
zip -r -qq ifcopenshell-modular-${VERSION}-${GITHUB_SHA:0:7}-pyodide.zip *.whl
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: build-logs-pyodide
path: |
@@ -65,7 +77,7 @@ jobs:
- name: Pack Dependencies
run: |
cd ifcopenshell_build
python ../IfcOpenShell/pyodide/cache_dependencies.py pack
python ../IfcOpenShell/nix/cache_dependencies.py pack
- name: Commit and Push Changes to Build Repository
run: |
@@ -86,3 +98,4 @@ jobs:
- name: Upload .zip archives to S3
run: |
aws s3 cp dist s3://ifcopenshell-builds/ --recursive --exclude "*" --include "*.whl"
aws s3 cp dist-modular s3://ifcopenshell-builds/ --recursive --exclude "*" --include "*.zip"
+165 -26
View File
@@ -6,20 +6,44 @@ on:
jobs:
build_ifcopenshell:
runs-on: ubuntu-22.04
container: rockylinux:8
container: rockylinux:9
steps:
- name: Set up uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Install Python
# Installs latest Python version so it's preferred by uv over Rocky's system Python.
run: uv python install
- name: Install Dependencies
run: |
yum update -y
yum install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \
dnf update -y
dnf install -y epel-release
# --enablerepo=crb: libstdc++-static (libsupc++.a, needed by the
# bundled FLTK link) lives in Rocky's CodeReady Builder repo, which
# is disabled by default.
dnf install -y --enablerepo=crb gcc gcc-c++ git autoconf automake bison make zip cmake python3 python3-pip \
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
findutils xz byacc
python3 -m pip install typing_extensions
findutils xz byacc patchelf libxkbcommon-devel \
python3.11 python3.11-pip \
dbus-devel \
libXext-devel libXinerama-devel libXcursor-devel libXrender-devel \
libXfixes-devel libXft-devel pango-devel cairo-devel libstdc++-static
python3 -m pip install aqtinstall
git config --global --add safe.directory '*'
- name: Install Rust
# The bonsaiviewer-autodesk connector is a Rust crate; the "Package
# .zip archives" step below runs `cargo build --release` via
# packaging/build.py. Match the dedicated connector workflow's stable
# toolchain (dtolnay/rust-toolchain@stable).
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Install aws cli
run: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
@@ -29,39 +53,41 @@ jobs:
aws --version
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/build-outputs
path: ./build
ref: rockylinux8-x64
ref: rockylinux9-x64
lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Unpack Dependencies
run: |
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
cd build
uv run ../nix/cache_dependencies.py unpack
- name: ccache
# TODO: Use tag after 1.2.20 releases.
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
uses: hendrikmuhs/ccache-action@v1.2.23
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
- name: Run Build Script
shell: bash
run: |
set -o pipefail
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
CXXFLAGS="-O3" CFLAGS="-O3" ADD_COMMIT_SHA=1 BUILD_CFG=Release BUILD_BONSAIVIEWER=ON \
uv run --with aqtinstall ./nix/build-all.py \
-v --diskcleanup --shared 2>&1 \
| tee build.log
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: build-logs-rocky
path: |
@@ -72,9 +98,7 @@ jobs:
- name: Pack Dependencies
run: |
cd build
for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do
test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir");
done
uv run ../nix/cache_dependencies.py pack
- name: Commit and Push Changes to Build Repository
run: |
@@ -86,10 +110,111 @@ jobs:
git push || true
- name: Package .zip archives
shell: bash
run: |
VERSION=v`cat VERSION`
# bonsaiviewer-autodesk is now a Rust connector. packaging/build.py
# invokes `cargo build --release` and stages the binary +
# connector.json into dist/autodesk/. Same on-disk shape as the
# old PyInstaller flow so the symlink + zip steps below
# continue to work unchanged.
python3.11 src/bonsaiviewer-autodesk/packaging/build.py
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
test -d "$autodesk_connector_dir"
cd ./build/`uname`/*/install/ifcopenshell
mkdir ~/output
mkdir -p ~/output
install_root="$PWD"
QT6_VERSION="${QT6_VERSION:-6.8.3}"
if [ -z "${QT_DIR:-}" ]; then
for qt_candidate in "$(dirname "$install_root")"/qt6-${QT6_VERSION}-*/${QT6_VERSION}/*; do
if [ -d "$qt_candidate/lib" ]; then
QT_DIR="$qt_candidate"
break
fi
done
fi
ensure_soname_links() {
dest="$1"
find "$dest" -maxdepth 1 -type f -name "*.so*" | while IFS= read -r shared_object; do
soname=$(readelf -d "$shared_object" 2>/dev/null | sed -n 's/.*(SONAME).*Shared library: \[\(.*\)\].*/\1/p' | head -n 1)
[ -n "$soname" ] || continue
[ -e "$dest/$soname" ] && continue
ln -s "$(basename "$shared_object")" "$dest/$soname"
done
}
stage_runtime_payload() {
dest="$1"
include_geometry_writers="${2:-1}"
while IFS= read -r runtime_file; do
if [ "$include_geometry_writers" != "1" ] && [[ "$(basename "$runtime_file")" == ifcopenshell.geometry.writer.* ]]; then
continue
fi
cp -P "$runtime_file" "$dest/"
done < <(
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
[ -d "$runtime_dir" ] || continue
find "$runtime_dir" \( -type f -o -type l \) \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
done
)
ensure_soname_links "$dest"
}
stage_qt_runtime_payload() {
exe_path="$1"
dest="$2"
[ -n "${QT_DIR:-}" ] && [ -d "$QT_DIR/lib" ] || return 0
if ! LD_LIBRARY_PATH="$QT_DIR/lib:${LD_LIBRARY_PATH:-}" ldd "$exe_path" 2>/dev/null | grep -q "libQt6"; then
return 0
fi
find "$QT_DIR/lib" -maxdepth 1 \( -type f -o -type l \) -name "*.so*" -exec cp -P {} "$dest/" \;
ensure_soname_links "$dest"
if [ -d "$QT_DIR/plugins" ]; then
pushd "$QT_DIR/plugins" > /dev/null
find . \( -type f -o -type l \) -name "*.so*" | while IFS= read -r plugin_file; do
mkdir -p "$dest/plugins/$(dirname "$plugin_file")"
cp -P "$plugin_file" "$dest/plugins/$plugin_file"
done
popd > /dev/null
if [ -d "$dest/plugins" ]; then
find "$dest/plugins" -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN/../..:$ORIGIN' {} \;
fi
fi
find "$dest" -maxdepth 1 -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN' {} \;
printf "[Paths]\nPrefix = .\n" > "$dest/qt.conf"
}
check_runtime_dependencies() {
package_dir="$1"
missing=0
while IFS= read -r binary_file; do
readelf -h "$binary_file" >/dev/null 2>&1 || continue
if ! env -u LD_LIBRARY_PATH ldd "$binary_file" > "$package_dir/.ldd.out" 2>&1; then
echo "ldd failed for $binary_file"
cat "$package_dir/.ldd.out"
missing=1
continue
fi
if grep -q "not found" "$package_dir/.ldd.out"; then
echo "Missing runtime dependencies for $binary_file"
grep "not found" "$package_dir/.ldd.out"
missing=1
fi
done < <(find "$package_dir" -type f \( -perm /111 -o -name "*.so" -o -name "*.so.*" \))
rm -f "$package_dir/.ldd.out"
if [ "$missing" -ne 0 ]; then
echo "Runtime dependency check found issues; continuing packaging."
fi
return 0
}
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
@@ -104,18 +229,32 @@ jobs:
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell/*
stage_runtime_payload ifcopenshell
zip -y -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell
mv *.zip ~/output
popd > /dev/null
done
cd bin
rm *.zip || true
ls | while read exe; do
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip $exe
rm -f "$install_root"/bin/*.zip
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
exe=`basename "$exe_path"`
package_dir="$install_root/.package-${exe}"
rm -rf "$package_dir"
mkdir -p "$package_dir"
cp "$exe_path" "$package_dir/"
patchelf --set-rpath '$ORIGIN' "$package_dir/$exe"
stage_runtime_payload "$package_dir" 0
stage_qt_runtime_payload "$exe_path" "$package_dir"
if [ "$exe" = "BonsaiViewer" ]; then
mkdir -p "$package_dir/connectors"
cp -a "$autodesk_connector_dir" "$package_dir/connectors/"
fi
check_runtime_dependencies "$package_dir"
pushd "$package_dir" > /dev/null
zip -y -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip" .
popd > /dev/null
rm -rf "$package_dir"
done
mv *.zip ~/output
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
+176 -26
View File
@@ -6,20 +6,55 @@ on:
jobs:
build_ifcopenshell:
runs-on: ubuntu-22.04-arm
container: arm64v8/rockylinux:8
# Rocky 10 (glibc 2.39) — aqt's official Qt6 ARM binaries are built
# against glibc 2.38, which Rocky 9 (glibc 2.34) can't run (moc fails to
# load). The legacy arm64v8/rockylinux Docker image stopped at 9; Rocky 10
# is published under the rockylinux/rockylinux namespace.
container:
image: rockylinux/rockylinux:10
# The community rockylinux image omits PATH from its config (the old
# Docker Official arm64v8/rockylinux set it), so GitHub Actions `run:`
# steps fail with `exec: "sh": not found` — docker exec has no /usr/bin
# to resolve the shell. Restore a standard PATH; GITHUB_PATH prepends
# (uv, cargo) are still layered on top by the runner.
env:
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
steps:
- name: Set up uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Install Python
# Installs latest Python version so it's preferred by uv over Rocky's system Python.
run: uv python install
- name: Install Dependencies
run: |
yum update -y
yum install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \
dnf update -y
dnf install -y epel-release
# --enablerepo=crb: libstdc++-static (libsupc++.a, needed by the
# bundled FLTK link) lives in Rocky's CodeReady Builder repo, which
# is disabled by default.
dnf install -y --enablerepo=crb gcc gcc-c++ git autoconf automake bison make zip cmake python3 python3-pip \
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
findutils xz byacc
python3 -m pip install typing_extensions
findutils xz byacc patchelf libxkbcommon-devel \
dbus-devel \
libXext-devel libXinerama-devel libXcursor-devel libXrender-devel \
libXfixes-devel libXft-devel pango-devel cairo-devel libstdc++-static
python3 -m pip install aqtinstall
git config --global --add safe.directory '*'
- name: Install Rust
# The bonsaiviewer-autodesk connector is a Rust crate; the "Package
# .zip archives" step below runs `cargo build --release` via
# packaging/build.py. Match the dedicated connector workflow's stable
# toolchain (dtolnay/rust-toolchain@stable).
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Install aws cli
run: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip"
@@ -29,39 +64,41 @@ jobs:
aws --version
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/build-outputs
path: ./build
ref: rockylinux8-arm64
ref: rockylinux9-arm64
lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Unpack Dependencies
run: |
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
cd build
uv run ../nix/cache_dependencies.py unpack
- name: ccache
# TODO: Use tag after 1.2.20 releases.
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
uses: hendrikmuhs/ccache-action@v1.2.23
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
key: ubuntu-22.04-${{ runner.arch }}-rockylinux10
- name: Run Build Script
shell: bash
run: |
set -o pipefail
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
CXXFLAGS="-O3" CFLAGS="-O3" ADD_COMMIT_SHA=1 BUILD_CFG=Release BUILD_BONSAIVIEWER=ON \
uv run --with aqtinstall ./nix/build-all.py \
-v --diskcleanup --shared 2>&1 \
| tee build.log
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: build-logs-rocky-arm64
path: |
@@ -72,9 +109,7 @@ jobs:
- name: Pack Dependencies
run: |
cd build
for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do
test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir");
done
uv run ../nix/cache_dependencies.py pack
- name: Commit and Push Changes to Build Repository
run: |
@@ -86,10 +121,111 @@ jobs:
git push || true
- name: Package .zip archives
shell: bash
run: |
VERSION=v`cat VERSION`
# bonsaiviewer-autodesk is now a Rust connector. packaging/build.py
# invokes `cargo build --release` and stages the binary +
# connector.json into dist/autodesk/. Same on-disk shape as the
# old PyInstaller flow so the symlink + zip steps below
# continue to work unchanged.
python3 src/bonsaiviewer-autodesk/packaging/build.py
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
test -d "$autodesk_connector_dir"
cd ./build/`uname`/*/install/ifcopenshell
mkdir ~/output
mkdir -p ~/output
install_root="$PWD"
QT6_VERSION="${QT6_VERSION:-6.8.3}"
if [ -z "${QT_DIR:-}" ]; then
for qt_candidate in "$(dirname "$install_root")"/qt6-${QT6_VERSION}-*/${QT6_VERSION}/*; do
if [ -d "$qt_candidate/lib" ]; then
QT_DIR="$qt_candidate"
break
fi
done
fi
ensure_soname_links() {
dest="$1"
find "$dest" -maxdepth 1 -type f -name "*.so*" | while IFS= read -r shared_object; do
soname=$(readelf -d "$shared_object" 2>/dev/null | sed -n 's/.*(SONAME).*Shared library: \[\(.*\)\].*/\1/p' | head -n 1)
[ -n "$soname" ] || continue
[ -e "$dest/$soname" ] && continue
ln -s "$(basename "$shared_object")" "$dest/$soname"
done
}
stage_runtime_payload() {
dest="$1"
include_geometry_writers="${2:-1}"
while IFS= read -r runtime_file; do
if [ "$include_geometry_writers" != "1" ] && [[ "$(basename "$runtime_file")" == ifcopenshell.geometry.writer.* ]]; then
continue
fi
cp -P "$runtime_file" "$dest/"
done < <(
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
[ -d "$runtime_dir" ] || continue
find "$runtime_dir" \( -type f -o -type l \) \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
done
)
ensure_soname_links "$dest"
}
stage_qt_runtime_payload() {
exe_path="$1"
dest="$2"
[ -n "${QT_DIR:-}" ] && [ -d "$QT_DIR/lib" ] || return 0
if ! LD_LIBRARY_PATH="$QT_DIR/lib:${LD_LIBRARY_PATH:-}" ldd "$exe_path" 2>/dev/null | grep -q "libQt6"; then
return 0
fi
find "$QT_DIR/lib" -maxdepth 1 \( -type f -o -type l \) -name "*.so*" -exec cp -P {} "$dest/" \;
ensure_soname_links "$dest"
if [ -d "$QT_DIR/plugins" ]; then
pushd "$QT_DIR/plugins" > /dev/null
find . \( -type f -o -type l \) -name "*.so*" | while IFS= read -r plugin_file; do
mkdir -p "$dest/plugins/$(dirname "$plugin_file")"
cp -P "$plugin_file" "$dest/plugins/$plugin_file"
done
popd > /dev/null
if [ -d "$dest/plugins" ]; then
find "$dest/plugins" -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN/../..:$ORIGIN' {} \;
fi
fi
find "$dest" -maxdepth 1 -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN' {} \;
printf "[Paths]\nPrefix = .\n" > "$dest/qt.conf"
}
check_runtime_dependencies() {
package_dir="$1"
missing=0
while IFS= read -r binary_file; do
readelf -h "$binary_file" >/dev/null 2>&1 || continue
if ! env -u LD_LIBRARY_PATH ldd "$binary_file" > "$package_dir/.ldd.out" 2>&1; then
echo "ldd failed for $binary_file"
cat "$package_dir/.ldd.out"
missing=1
continue
fi
if grep -q "not found" "$package_dir/.ldd.out"; then
echo "Missing runtime dependencies for $binary_file"
grep "not found" "$package_dir/.ldd.out"
missing=1
fi
done < <(find "$package_dir" -type f \( -perm /111 -o -name "*.so" -o -name "*.so.*" \))
rm -f "$package_dir/.ldd.out"
if [ "$missing" -ne 0 ]; then
echo "Runtime dependency check found issues; continuing packaging."
fi
return 0
}
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
@@ -104,18 +240,32 @@ jobs:
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip ifcopenshell/*
stage_runtime_payload ifcopenshell
zip -y -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip ifcopenshell
mv *.zip ~/output
popd > /dev/null
done
cd bin
rm *.zip || true
ls | while read exe; do
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip $exe
rm -f "$install_root"/bin/*.zip
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
exe=`basename "$exe_path"`
package_dir="$install_root/.package-${exe}"
rm -rf "$package_dir"
mkdir -p "$package_dir"
cp "$exe_path" "$package_dir/"
patchelf --set-rpath '$ORIGIN' "$package_dir/$exe"
stage_runtime_payload "$package_dir" 0
stage_qt_runtime_payload "$exe_path" "$package_dir"
if [ "$exe" = "BonsaiViewer" ]; then
mkdir -p "$package_dir/connectors"
cp -a "$autodesk_connector_dir" "$package_dir/connectors/"
fi
check_runtime_dependencies "$package_dir"
pushd "$package_dir" > /dev/null
zip -y -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip" .
popd > /dev/null
rm -rf "$package_dir"
done
mv *.zip ~/output
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
+49 -14
View File
@@ -5,23 +5,38 @@ on:
jobs:
build_ifcopenshell:
runs-on: windows-2022
strategy:
fail-fast: false
matrix:
arch: ['x64']
include:
- arch: x64
runs_on: windows-2022
deps_dir: _deps-vs2022-x64-installed
vcvars: '"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"'
build_branch: windows-x64
zip_suffix: win64
- arch: ARM64
runs_on: windows-11-arm
deps_dir: _deps-vs2022-ARM64-installed
vcvars: '"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsarm64.bat"'
build_branch: windows-arm64
zip_suffix: win-arm64
runs-on: ${{ matrix.runs_on }}
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/build-outputs
path: _deps-vs2022-x64-installed
ref: windows-${{ matrix.arch }}
path: ${{ matrix.deps_dir }}
ref: ${{ matrix.build_branch }}
lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }}
@@ -31,30 +46,49 @@ jobs:
- name: Unpack Dependencies
run: |
cd _deps-vs2022-x64-installed
cd ${{ matrix.deps_dir }}
Get-ChildItem -Path . -Filter 'cache-*.zip' | ForEach-Object {
7z x $_.FullName
Write-Host "Extracting $($_.Name)"
7z x -bso0 -bsp0 $_.FullName
if ($LASTEXITCODE -ne 0) {
throw "Failed to extract $($_.Name) with 7z exit code $LASTEXITCODE."
}
}
- name: ccache
# TODO: Use tag after 1.2.20 releases.
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
uses: hendrikmuhs/ccache-action@v1.2.23
with:
key: win-${{ matrix.arch }}
# Windows ccache needs ~1GB
# and with default 500MB some cache gets deleted, leading to misses.
max-size: 5000MB
- name: Set up Python for connector build
uses: actions/setup-python@v6
with:
python-version: '3.12'
# Build the Autodesk connector before the C++ build: build-all-win.py
# bundles it next to BonsaiViewer.exe while archiving the executables.
# bonsaiviewer-autodesk is a Rust connector; packaging/build.py runs
# `cargo build --release` and stages the binary + connector.json into
# dist/autodesk/.
- name: Build Autodesk connector
working-directory: src/bonsaiviewer-autodesk
run: python packaging/build.py
- name: Run Build Script And Pack .zip Archives
shell: cmd
env:
TARGET_ARCH: ${{ matrix.arch }} # lets the Python script know which arch to target (optional override)
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
call ${{ matrix.vcvars }}
cd win
python build-all-win.py
- name: Pack Dependencies
run: |
cd _deps-vs2022-x64-installed
cd ${{ matrix.deps_dir }}
Get-ChildItem -Path . -Directory | ForEach-Object {
$cacheFile = "cache-$($_.Name).zip"
echo $cacheFile
@@ -65,12 +99,13 @@ jobs:
- name: Commit and Push Changes to Build Repository
run: |
cd _deps-vs2022-x64-installed
cd ${{ matrix.deps_dir }}
git config user.name "IfcOpenBot"
git config user.email "ifcopenbot@ifcopenshell.org"
git checkout -B ${{ matrix.build_branch }}
git add *.zip
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
git push || echo "Push failed"
git push --set-upstream origin ${{ matrix.build_branch }} || echo "Push failed"
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v6
+2 -2
View File
@@ -19,8 +19,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6 # https://github.com/actions/checkout
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7 # https://github.com/actions/checkout
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-tags: true
fetch-depth: 0
+16 -17
View File
@@ -24,9 +24,15 @@ jobs:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
outputs:
timestamp: ${{ steps.timestamp.outputs.timestamp }}
steps:
- name: Set env
run: echo ok go
- name: Get current timestamp
id: timestamp
# Include hours and minutes to release tag
# to avoid possibility of unstable repo's index.json
# pointing to the new file when index.json itself wasn't yet updated.
run: echo "timestamp=$(date +'%y%m%d%H%M')" >> $GITHUB_OUTPUT
build:
needs: activate
@@ -59,20 +65,14 @@ jobs:
config:
short_name: macos
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
- name: Get current version
id: version
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Get current date
id: date
# Include hours and minutes to release tag
# to avoid possibility of unstable repo's index.json
# pointing to the new file when index.json itself wasn't yet updated.
run: echo "date=$(date +'%y%m%d%H%M')" >> $GITHUB_OUTPUT
- name: Compile
run: |
cd src/bonsai && make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }}
@@ -88,8 +88,8 @@ jobs:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ steps.find_zip.outputs.filepath }}
asset_name: ${{ steps.find_zip.outputs.filename }}
release_name: "bonsai-${{steps.version.outputs.version}}-alpha${{steps.date.outputs.date}} (unstable)"
tag: "bonsai-${{steps.version.outputs.version}}-alpha${{steps.date.outputs.date}}"
release_name: "bonsai-${{steps.version.outputs.version}}-alpha${{ needs.activate.outputs.timestamp }} (unstable)"
tag: "bonsai-${{steps.version.outputs.version}}-alpha${{ needs.activate.outputs.timestamp }}"
overwrite: true
body: "See README in https://github.com/IfcOpenShell/bonsai_unstable_repo/ on how to setup autoupdates for daily Bonsai builds."
@@ -98,7 +98,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout bonsai_unstable_repo repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/bonsai_unstable_repo
token: ${{ secrets.IFCOPENBOT_TOKEN }}
@@ -109,7 +109,7 @@ jobs:
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
# Download Blender.
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.0/blender-5.0.1-linux-x64.tar.xz
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.2/blender-5.2.0-linux-x64.tar.xz
tar -xf blender.tar.xz
# Setup Blender.
@@ -122,7 +122,7 @@ jobs:
pip install -r requirements.txt
python setup_extensions_repo.py --last-tag
cd ..
bonsai_zip="$(pwd)/$(ls bonsai_unstable_repo/bonsai_py311*-linux-x64.zip)"
bonsai_zip="$(pwd)/$(ls bonsai_unstable_repo/bonsai_py313*-linux-x64.zip)"
# Install Bonsai.
blender --command extension install-file -r user_default -e $bonsai_zip
@@ -179,8 +179,7 @@ jobs:
blender --online-mode --command extension install --enable --sync sun_position
cd IfcOpenShell/src/bonsai
pip install pytest-blender
pip install pytest-bdd
pip install -r requirements-dev.txt
blender --background --python scripts/setup_pytest.py
blender --python-expr "import bonsai; print(bonsai.bbim_semver); import ifcopenshell; print(ifcopenshell.version)" --background
make test
+8 -3
View File
@@ -24,7 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py311, py312]
pyver: [py311, py312, py313]
config:
- {
name: "Windows Build",
@@ -42,9 +42,14 @@ jobs:
name: "MacOS ARM Build",
short_name: macosm1,
}
exclude:
# Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0.
- pyver: py313
config:
short_name: macos
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -37,8 +37,8 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+35
View File
@@ -0,0 +1,35 @@
name: ci-ifcedit-pypi
on:
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: '3.11'
- name: Compile
run: |
pip install build
cd src/ifcedit &&
make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcedit/dist
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+36
View File
@@ -0,0 +1,36 @@
name: ci-ifcmcp-pypi
on:
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: '3.11'
- name: Compile
run: |
pip install build
cd src/ifcmcp &&
make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcmcp/dist
verbose: true
@@ -24,7 +24,7 @@ jobs:
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
- uses: mamba-org/setup-micromamba@v3 # https://github.com/mamba-org/setup-micromamba
with:
environment-name: test-env
create-args: >-
@@ -21,7 +21,7 @@ jobs:
date: ${{ steps.date.outputs.date }}
verdate: ${{ steps.verdate.outputs.verdate }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Set env
run: echo ok go
@@ -75,7 +75,7 @@ jobs:
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
fi
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
@@ -84,7 +84,7 @@ jobs:
run: |
curl -L https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.13.sdk.tar.xz | tar -xvJf - -C /Users/runner/work/
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
- uses: mamba-org/setup-micromamba@v3 # https://github.com/mamba-org/setup-micromamba
with:
environment-name: test-env
create-args: >-
+10 -11
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-22.04
needs: activate
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
@@ -31,11 +31,11 @@ jobs:
sudo apt-get install --no-install-recommends \
git cmake gcc g++ libboost-all-dev python3-all-dev swig libpcre3-dev libxml2-dev \
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
libhdf5-dev libcgal-dev nlohmann-json3-dev libeigen3-dev
libcgal-dev nlohmann-json3-dev libeigen3-dev
-
name: ccache
uses: hendrikmuhs/ccache-action@v1.2
uses: hendrikmuhs/ccache-action@v1.2.23
-
name: Build ifcopenshell
@@ -60,7 +60,6 @@ jobs:
-DMPFR_INCLUDE_DIR=/usr/include \
-DGMP_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DMPFR_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DHDF5_INCLUDE_DIR=/usr/include/hdf5/serial \
-DGLTF_SUPPORT=On \
-DJSON_INCLUDE_DIR=/usr/include \
-DEIGEN_DIR=/usr/include/eigen3 \
@@ -73,7 +72,7 @@ jobs:
make package
working-directory: build
- name: Upload
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
# Artifact name
name: ifcos-artifacts
@@ -86,31 +85,31 @@ jobs:
name: Docker Build, Tag, Push
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
lfs: true
- name: Download
uses: actions/download-artifact@v7.0.0
uses: actions/download-artifact@v8.0.1
with:
# Artifact name
name: ifcos-artifacts
path: artifacts/
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
-
name: Login to Dockerhub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: aecgeeks
password: ${{ secrets.DOCKER_HUB_TOKEN }}
-
name: Build container image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: artifacts
repository: aecgeeks/ifcopenshell
@@ -24,7 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311, py312, py313, py314]
pyver: [py310, py311, py312, py313, py314]
config:
- {
name: "Windows 64bit",
@@ -47,10 +47,10 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+3 -3
View File
@@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311, py312, py313, py314]
pyver: [py310, py311, py312, py313, py314]
config:
- {
name: "Windows 64bit",
@@ -38,10 +38,10 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+35
View File
@@ -0,0 +1,35 @@
name: ci-ifcquery-pypi
on:
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: '3.11'
- name: Compile
run: |
pip install build
cd src/ifcquery &&
make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcquery/dist
+2 -2
View File
@@ -25,8 +25,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
+2 -2
View File
@@ -19,8 +19,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
+2 -2
View File
@@ -10,9 +10,9 @@ jobs:
publish_website:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Checkout ifctester_org_static_html
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/ifctester_org_static_html
token: ${{ secrets.IFCOPENBOT_TOKEN }}
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+155
View File
@@ -0,0 +1,155 @@
# This file was generated with the assistance of an AI coding tool.
name: ci-ifcwrap-standalone
on:
workflow_dispatch:
pull_request:
paths:
- ".github/workflows/ci-ifcwrap-standalone.yml"
- "cmake/**"
- "src/ifcwrap/**"
- "src/ifcparse/**"
- "src/ifcgeom/**"
- "src/serializers/**"
- "src/ifcconvert/**"
- "src/ifcopenshell-python/**"
- "src/svgfill/**"
push:
paths:
- ".github/workflows/ci-ifcwrap-standalone.yml"
- "cmake/**"
- "src/ifcwrap/**"
- "src/ifcparse/**"
- "src/ifcgeom/**"
- "src/serializers/**"
- "src/ifcconvert/**"
- "src/ifcopenshell-python/**"
- "src/svgfill/**"
env:
IFCOPENSHELL_PREFIX: ${{ github.workspace }}/ifcopenshell-install
jobs:
build-ifcopenshell:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
- name: Install C++ dependencies
run: |
sudo apt update
sudo apt-get install --no-install-recommends -y \
cmake \
gcc \
g++ \
libboost-date-time-dev \
libboost-filesystem-dev \
libboost-iostreams-dev \
libboost-program-options-dev \
libboost-regex-dev \
libboost-system-dev \
libboost-thread-dev \
libeigen3-dev \
libocct-data-exchange-dev \
libocct-draw-dev \
libocct-foundation-dev \
libocct-modeling-algorithms-dev \
libocct-modeling-data-dev \
libocct-ocaf-dev \
libocct-visualization-dev \
libpcre3-dev \
libtbb-dev \
libxml2-dev \
libxi-dev \
occt-misc \
tcl-dev \
tk-dev \
swig
- name: Configure minimal IfcOpenShell
run: |
cmake -S cmake -B build-ifcopenshell \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${IFCOPENSHELL_PREFIX}" \
-DCMAKE_PREFIX_PATH=/usr \
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
-DMINIMAL_BUILD=ON \
-DBUILD_IFCPYTHON=OFF \
"-DSCHEMA_VERSIONS=4x3_add2"
- name: Build and install minimal IfcOpenShell
run: |
cmake --build build-ifcopenshell --target install -j "$(nproc)"
- name: Set up Python 3.11
uses: actions/setup-python@v7
with:
python-version: 3.11
- name: Install Python import dependencies
run: |
python -m pip install --upgrade pip
python -m pip install numpy typing_extensions
- name: Configure standalone IfcPython
run: |
PYTHON_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')"
PYTHON_INCLUDE_DIR="$(python -c 'import sysconfig; print(sysconfig.get_path("include"))')"
cmake -S src/ifcwrap -B "build-ifcwrap-311" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="${IFCOPENSHELL_PREFIX};/usr" \
-DPython_EXECUTABLE:FILEPATH="${PYTHON_EXECUTABLE}" \
-DPython_INCLUDE_DIR:PATH="${PYTHON_INCLUDE_DIR}" \
-DPYTHON_EXECUTABLE:FILEPATH="${PYTHON_EXECUTABLE}" \
-DPYTHON_INCLUDE_DIR:PATH="${PYTHON_INCLUDE_DIR}"
- name: Build and install standalone IfcPython
run: |
cmake --build "build-ifcwrap-311" --target install -j "$(nproc)"
- name: Import installed IfcPython
run: |
PYTHONPATH="${RUNNER_TEMP}/ifcopenshell-python" python - <<'PY'
import ifcopenshell
print("IfcOpenShell import ok:", ifcopenshell.version)
PY
- name: Set up Python 3.12
uses: actions/setup-python@v7
with:
python-version: 3.12
- name: Install Python import dependencies
run: |
python -m pip install --upgrade pip
python -m pip install numpy typing_extensions
- name: Configure standalone IfcPython
run: |
PYTHON_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')"
PYTHON_INCLUDE_DIR="$(python -c 'import sysconfig; print(sysconfig.get_path("include"))')"
cmake -S src/ifcwrap -B "build-ifcwrap-312" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="${IFCOPENSHELL_PREFIX};/usr" \
-DPython_EXECUTABLE:FILEPATH="${PYTHON_EXECUTABLE}" \
-DPython_INCLUDE_DIR:PATH="${PYTHON_INCLUDE_DIR}" \
-DPYTHON_EXECUTABLE:FILEPATH="${PYTHON_EXECUTABLE}" \
-DPYTHON_INCLUDE_DIR:PATH="${PYTHON_INCLUDE_DIR}"
- name: Build and install standalone IfcPython
run: |
cmake --build "build-ifcwrap-312" --target install -j "$(nproc)"
- name: Import installed IfcPython
run: |
PYTHONPATH="${RUNNER_TEMP}/ifcopenshell-python" python - <<'PY'
import ifcopenshell
print("IfcOpenShell import ok:", ifcopenshell.version)
PY
@@ -1,4 +1,4 @@
name: ci-black-formatting
name: ci-lint
on:
push:
@@ -7,34 +7,37 @@ on:
jobs:
lint-formatting:
runs-on: ubuntu-latest
env:
MIN_IOS_PY_VERSION: "3.10"
MIN_BLENDER_PY_VERSION: "3.11"
steps:
- name: Action - checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Action - install python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: "3.10"
python-version: ${{ env.MIN_IOS_PY_VERSION }}
- name: Action - install python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: "3.11"
python-version: ${{ env.MIN_BLENDER_PY_VERSION }}
- name: Install dependencies
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install ruff
uv tool install black
uv tool install poethepoet
cat requirements-tools.txt | xargs -L1 uv tool install
# black doesn't catch all syntax errors, so we check them explicitly.
- name: Check syntax errors
id: syntax-errors
run: |
ERROR=0
python3.10 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
python3.11 -W error -m compileall -q src/bonsai || ERROR=1
# Using 2 Python versions - one minimum required for IfcOpenShell
# and other that's used by Blender currently.
python${{ env.MIN_IOS_PY_VERSION }} -W error -m compileall -q src/ifcopenshell-python || ERROR=1
python${{ env.MIN_BLENDER_PY_VERSION }} -W error -m compileall -q src/bonsai || ERROR=1
exit $ERROR
continue-on-error: true
@@ -52,6 +55,19 @@ jobs:
black --diff --check . | black-codeclimate | python .github/workflows/black_to_github_annotations.py
continue-on-error: true
- name: ty check (venv setup)
run: poe ty-venv
- name: ty check (bonsai)
id: ty-bonsai
run: poe ty-bonsai
continue-on-error: true
- name: ty check (ios)
id: ty-ios
run: poe ty-ios
continue-on-error: true
- name: Ruff check
id: ruff
run: |
@@ -82,8 +98,7 @@ jobs:
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
}
run_check poe ruff-main
run_check poe ruff-old
run_check poe ruff
exit $ERROR
continue-on-error: true
@@ -100,4 +115,10 @@ jobs:
if [ "${{ steps.ruff.outcome }}" != "success" ]; then
echo "::error::Ruff check failed, see Summary or 'ruff' step for the details." && ERROR=1
fi
if [ "${{ steps.ty-bonsai.outcome }}" != "success" ]; then
echo "::error::ty check (bonsai) failed, see 'ty check (bonsai)' step for the details." && ERROR=1
fi
if [ "${{ steps.ty-ios.outcome }}" != "success" ]; then
echo "::error::ty check (ios) failed, see 'ty check (ios)' step for the details." && ERROR=1
fi
exit $ERROR
@@ -0,0 +1,46 @@
name: Release Pyodide WASM Wheel
on:
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout IfcOpenShell
uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Build wheel
working-directory: pyodide
run: uv run pack_wheel.py --build
- name: Find wheel
id: wheel
run: |
WHEEL=$(ls pyodide/dist/ifcopenshell-*.whl)
echo "path=$WHEEL" >> $GITHUB_OUTPUT
echo "name=$(basename $WHEEL)" >> $GITHUB_OUTPUT
- name: Checkout wasm-wheels
uses: actions/checkout@v7
with:
repository: IfcOpenShell/wasm-wheels
path: wasm-wheels
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Commit and push wheel to wasm-wheels
run: |
WHEEL_NAME="${{ steps.wheel.outputs.name }}"
cp "${{ steps.wheel.outputs.path }}" "wasm-wheels/$WHEEL_NAME"
cd wasm-wheels
git config user.name "IfcOpenBot"
git config user.email "ifcopenbot@ifcopenshell.org"
git add "$WHEEL_NAME"
git commit -m "Add $WHEEL_NAME"
VERSION=$(cat ../VERSION)
git tag "v${VERSION}"
git push origin main
git push origin "v${VERSION}"
+37 -33
View File
@@ -10,11 +10,14 @@ on:
- 'src/ifcgeomserver/**'
- 'src/ifcjni/**'
- 'src/ifcmax/**'
- 'src/ifc5d/**'
- 'src/ifcedit/**'
- 'src/ifcmcp/**'
- 'src/ifcopenshell-python/**'
- '!src/ifcopenshell-python/docs/**'
- 'src/ifcparse/**'
- 'src/ifcquery/**'
- 'src/ifcwrap/**'
- 'src/qtviewer/**'
- 'src/svgfill/**'
- 'src/serializers/**'
- 'conda/**'
@@ -34,24 +37,28 @@ jobs:
compile-and-test:
runs-on: ubuntu-22.04
needs: activate
strategy:
fail-fast: false
matrix:
build_shared_libs: [ON, OFF]
env:
# Colored output for cmake.
CLICOLOR_FORCE: "1"
CMAKE_COLOR_DIAGNOSTICS: "ON"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: 3.11
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely pyparsing psutil
pip install src/bcf --no-deps
pip install pytest-xdist==3.8.0
@@ -76,13 +83,10 @@ jobs:
libtbb-dev nlohmann-json3-dev \
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
${OCCT_CMAKE_DEPS} \
libhdf5-dev libcgal-dev libeigen3-dev
libcgal-dev libeigen3-dev
- name: ccache
# TODO: temporarily pointing to 1.2.19 to get notified by dependabot when 1.2.20 is released
# to update hardcoded references to commits in some other workflows.
# Then we can switch back to 1.2 in all actions.
uses: hendrikmuhs/ccache-action@v1.2.19
uses: hendrikmuhs/ccache-action@v1.2.23
with:
key: ubuntu-22.04-${{ runner.arch }}
@@ -159,7 +163,7 @@ jobs:
# Remove default swig to avoid conflicts.
sudo apt remove --purge swig swig4.0
sudo apt-get install -y libpcre2-dev bison
git clone https://github.com/swig/swig --branch v4.1.0 --depth 1
git clone https://github.com/swig/swig --branch v4.2.1 --depth 1
cd swig
mkdir build && cd build
cmake .. \
@@ -181,9 +185,11 @@ jobs:
-DPYTHON_EXECUTABLE:FILEPATH=${{ env.pythonLocation }}/bin/python \
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
-DUSE_MMAP=On \
-DBUILD_SHARED_LIBS=${{ matrix.build_shared_libs }} \
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
-DGLTF_SUPPORT=On \
-DWITH_ROCKSDB=On \
-DBUILD_EXAMPLES=ON \
../cmake
sudo make -j $(nproc)
sudo make install
@@ -215,29 +221,11 @@ jobs:
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
cmake --build .
./arbitrary_open_profile_def && test -f arbitrary_open_profile_def.ifc
./composite_profile_def && test -f composite_profile_def.ifc
./csg_primitive && test -f csg_primitive.ifc
./ellipse_pies && test -f ellipse_pies.ifc
./faces && test -f faces.ifc
./ifc_curve_rebar && test -f ifc_curve_rebar.ifc
./profiles
test -f IfcUShapeProfileDef.ifc
test -f IfcTShapeProfileDef.ifc
test -f IfcZShapeProfileDef.ifc
test -f IfcEllipseProfileDef.ifc
test -f IfcIShapeProfileDef.ifc
test -f IfcLShapeProfileDef.ifc
test -f IfcCShapeProfileDef.ifc
test -f IfcCircleProfileDef.ifc
test -f IfcRectangleProfileDef.ifc
test -f IfcTrapeziumProfileDef.ifc
./IfcParseExamples "../IfcParseExamples_test.ifc"
./IfcOpenHouse && test -f IfcOpenHouse.ifc
./IfcParseExamples IfcOpenHouse.ifc
./IfcAdvancedHouse && test -f IfcAdvancedHouse.ifc
./IfcAlignment && test -f IfcAlignment.ifc
./IfcSimplifiedAlignment && test -f IfcSimplifiedAlignment.ifc
./triangulated_faceset && test -f triangulated_faceset.ifc
./IfcAlignment && test -f FHWA_Bridge_Geometry_Alignment_Example.ifc
./IfcSimplifiedAlignment && test -f FHWA_Bridge_Geometry_Alignment_Example_Simplified.ifc
- name: Test ifcopenshell-python
run: |
@@ -254,12 +242,28 @@ jobs:
pip install deepdiff
cd ../ifcdiff && make test || ERROR=1
cd ../ifcpatch && make test || ERROR=1
pip install -e ../ifc5d --no-deps
pip install odfpy openpyxl
cd ../ifc5d && make test || ERROR=1
pip install -e ../ifcquery --no-deps
cd ../ifcquery && make test || ERROR=1
pip install -e ../ifcedit --no-deps
cd ../ifcedit && make test || ERROR=1
# Pinned <2: mcp 2.0.0 renamed mcp.server.fastmcp.FastMCP to
# mcp.server.mcpserver.MCPServer, which ifcmcp doesn't support yet.
pip install "mcp>=1.0,<2"
pip install -e ../ifcmcp --no-deps
cd ../ifcmcp && make test || ERROR=1
pip install -e ../ifctester --no-deps
cd ../ifctester && make test || ERROR=1
make build-ids-docs || ERROR=1
# Run mathutils related tests at the end to ensure no other code is relying on mathutils.
# mathutils only has pre-built wheels for Python 3.13+; skip on older versions.
cd ../ifcopenshell-python
pip install mathutils
make test-mathutils || ERROR=1
if python -c "import sys; sys.exit(0 if sys.version_info >= (3, 13) else 1)"; then
pip install mathutils
make test-mathutils || ERROR=1
fi
if [ $ERROR -ne 0 ]; then
echo "One or more tests failed";
exit 1;
@@ -11,10 +11,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: '3.x'
-36
View File
@@ -1,36 +0,0 @@
name: Build and Deploy Stable Documentation
on:
workflow_dispatch: # Manual trigger
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Install dependencies
run: |
cd src/bonsai/docs
pip install -r requirements.txt # Run pip install from the docs directory
- name: Build documentation
run: |
cd src/bonsai/docs
make html
- name: Deploy to GitHub Pages (Stable)
uses: peaceiris/actions-gh-pages@v4
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
external_repository: IfcOpenShell/bonsaibim_org_docs
publish_branch: main
cname: docs.bonsaibim.org
publish_dir: src/bonsai/docs/_build/html
+65
View File
@@ -0,0 +1,65 @@
name: Deploy AI chat App to static page repo
permissions:
id-token: write
pages: write
on:
push:
paths:
- 'src/ifcchat/**'
- '.github/workflows/publish-aichat-app.yaml'
branches:
- v0.8.0
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
steps:
- name: Checkout (recursive)
uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Checkout intermediate Pages repo
uses: actions/checkout@v7
with:
repository: IfcOpenShell/aichat_ifcopenshell_org_static_html
ref: gh-pages
path: output
token: ${{ secrets.WEBSITE_PUBLISH }}
- name: Sync demo app into target subfolder
run: |
rsync -av --delete --exclude='.git/' src/ifcchat/ output/
- name: Setup Python
uses: actions/setup-python@v7
with:
python-version: "3.x"
- name: Download wheels
working-directory: output/
run: |
pip download ifcquery==0.8.5 ifcopenshell-mcp==0.8.5 ifcedit==0.8.5 lark==1.3.1 isodate==0.7.2 --no-deps -d ./dist
- name: Commit and push if changed
working-directory: output
run: |
git config --global user.name 'IfcOpenBot'
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
git add .
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "$(git log --oneline -1)"
git push origin gh-pages
@@ -0,0 +1,16 @@
name: Publish Bonsai Releases
on:
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- run: uv run .github/scripts/publish-bonsai-releases.py
env:
BLENDER_EXTENSIONS_TOKEN: ${{ secrets.BLENDER_EXTENSIONS_TOKEN }}
+25 -18
View File
@@ -1,4 +1,4 @@
name: Deploy Pyodide Demo App to GitHub Pages
name: Deploy Pyodide Demo App to static page repo
permissions:
id-token: write
@@ -11,6 +11,7 @@ on:
- '.github/workflows/publish-pyodide-demo-app.yml'
branches:
- v0.8.0
workflow_dispatch:
jobs:
activate:
@@ -26,25 +27,31 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout (recursive)
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload static files as artifact
id: deployment
uses: actions/upload-pages-artifact@v4
- name: Checkout intermediate Pages repo
uses: actions/checkout@v7
with:
path: src/pyodide/demo-app/
repository: IfcOpenShell/wasm_ifcopenshell_org_static_html
ref: gh-pages
path: output
token: ${{ secrets.WEBSITE_PUBLISH }}
- name: Sync demo app into target subfolder
run: |
rsync -av --delete --exclude='.git/' src/pyodide/demo-app/ output/
- name: Commit and push if changed
working-directory: output
run: |
git config --global user.name 'IfcOpenBot'
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
git add .
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "$(git log --oneline -1)"
git push origin gh-pages
+2 -3
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
- name: Install C++ dependencies
@@ -36,7 +36,7 @@ jobs:
swig libpcre3-dev libxml2-dev \
libtbb-dev nlohmann-json3-dev \
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
libhdf5-dev libcgal-dev opencollada-dev
libcgal-dev opencollada-dev
- name: Build
env:
@@ -64,7 +64,6 @@ jobs:
-DMPFR_INCLUDE_DIR=/usr/include \
-DGMP_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DMPFR_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DHDF5_INCLUDE_DIR=/usr/include/hdf5/serial \
-DOPENCOLLADA_INCLUDE_DIR=/usr/include/opencollada \
-DOPENCOLLADA_LIBRARY_DIR=/usr/lib/opencollada/ \
../cmake
+30 -2
View File
@@ -4,14 +4,18 @@
/_deps-vs*-x*-installed/
/_installed-vs*-x*/
/build/
/build.log
/output/
/src/examples/build/
# ifctester docs output
/src/ifctester/test/build/
# output directories
/cmake/out/
/src/examples/out/
/src/ifcmax/out/
/src/ifcwrap/out/
/src/qtviewer/out/
/src/ifctester/webapp/public/pyodide/
/win/BuildDepsCache*.txt
@@ -19,12 +23,14 @@
__pycache__
*.py.bak
venv
uv.lock
# Visual Studio Code files
.vscode
!.vscode/launch.json
!.vscode/tasks.json
.vs
/*.code-workspace
# PyCharm files
.idea
@@ -80,8 +86,14 @@ src/ifcopenshell-python/test/build
# bonsai i18n
src/bonsai/bonsai/translations.py
# bonsai test temp files
# bonsai external dependencies (cloned for just ty checks)
src/bonsai/external_dependencies/
# bonsai test temp/cache files
src/bonsai/test/files/temp
src/bonsai/test/files/*.cache.blend
src/bonsai/test/files/*.cache.json
src/bonsai/test/files/*.cache.sqlite
# bonsai data
src/bonsai/bonsai/bim/data/build/
@@ -95,6 +107,11 @@ src/bonsai/bonsai/bim/data/webui/running_pid.json
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
# plugins
src/ifcopenshell-python/ifcopenshell/ifcopenshell.document.*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell.geometry.*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell.parse.schema*.so
# apple
.DS_Store
@@ -113,3 +130,14 @@ dev_environment.bat
src/ifcopenshell-python/ifcopenshell/express/*.exp
src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
# temp files from AI coding tools
*.claude
CLAUDE.local.md
*.py.tmp*
*.json.tmp*
# bonsaiviewer-autodesk connector build artifacts
/src/bonsaiviewer-autodesk/build/
/src/bonsaiviewer-autodesk/dist/
+1 -1
View File
@@ -27,7 +27,7 @@ RUN echo "deb http://archive.ubuntu.com/ubuntu focal-proposed main restricted" |
libboost-all-dev \
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev \
libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
libhdf5-serial-dev python3-pytest ; \
python3-pytest ; \
rm -rf /var/lib/apt/lists/* ;
COPY . /home/IfcOpenShell/
+6 -4
View File
@@ -18,7 +18,7 @@ and many other libraries, CLI apps, and more. Support is also provided for auxil
For more information, see:
* [IfcOpenShell Website](http://ifcopenshell.org)
* [IfcOpenShell Website](https://ifcopenshell.org)
* [IfcOpenShell Documentation](https://docs.ifcopenshell.org)
* [IfcOpenShell C++ Installation](https://docs.ifcopenshell.org/ifcopenshell/installation.html)
* [IfcOpenShell Python Installation](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html)
@@ -50,11 +50,14 @@ Contents
| [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcconvert/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcconvert-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
| [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccsv?label=PyPI&color=006dad)](https://pypi.org/project/ifccsv/) |
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcdiff?label=PyPI&color=006dad)](https://pypi.org/project/ifcdiff/) |
| [ifcedit](https://docs.ifcopenshell.org/ifcedit.html) | CLI wrapper for ifcopenshell.api IFC model mutation functions | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcedit?label=PyPI&color=006dad)](https://pypi.org/project/ifcedit/) |
| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcfm?label=PyPI&color=006dad)](https://pypi.org/project/ifcfm/) |
| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcmax.html)
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) [Pyodide WASM Wheels](https://github.com/IfcOpenShell/wasm-wheels#pyodide-test-wheels) |
| [ifcmcp](https://docs.ifcopenshell.org/ifcmcp.html) | MCP server for querying and editing IFC building models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcopenshell-mcp?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell-mcp/) |
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) [![Pyodide WASM Wheels tag](https://img.shields.io/github/v/tag/ifcopenshell/wasm-wheels?sort=semver&label=pyodide-wasm-wheels)](https://github.com/IfcOpenShell/wasm-wheels) |
| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcpatch?label=PyPI&color=006dad)](https://pypi.org/project/ifcpatch/) |
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*.*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
| [ifcquery](https://docs.ifcopenshell.org/ifcquery.html) | CLI tool for querying and inspecting IFC building models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcquery?label=PyPI&color=006dad)](https://pypi.org/project/ifcquery/) |
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifctester?label=PyPI&color=006dad)](https://pypi.org/project/ifctester/) |
The IfcOpenShell C++ codebase is split into multiple interal libraries:
@@ -67,7 +70,6 @@ The IfcOpenShell C++ codebase is split into multiple interal libraries:
| ifcjni | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| ifcparse | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| ifcwrap | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| qtviewer | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| serializers | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
[LGPL]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/COPYING.LESSER "LGPL-3.0-or-later"
+1 -1
View File
@@ -1 +1 @@
0.8.5
0.8.6
+1 -1
View File
@@ -3,7 +3,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
<metadata>
<id>blenderbim-nightly</id>
<version>blenderbim_build_version-alpha</version>
<version>blenderbim_build_version</version>
<packageSourceUrl>https://github.com/IfcOpenShell/IfcOpenShell</packageSourceUrl>
<owners>fbpyr</owners>
<!-- == SOFTWARE SPECIFIC SECTION == -->
+33 -21
View File
@@ -3,16 +3,18 @@
apt update && apt install git wget curl ptpython mono-devel micro
mkdir -p /home/runner/work/IfcOpenShell && cd /home/runner/work/IfcOpenShell
git clone https://github.com/IfcOpenShell/IfcOpenShell
cd /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/
cd /home/runner/work/IfcOpenShell/IfcOpenShell/choco/bonsai/
micro choco_release.py # paste this script, comment out push command
export CHOCO_TOKEN="secret_choco_release_token"
python3 choco_release.py
"""
import datetime
import hashlib
import os
import pathlib
import re
import subprocess
from typing import NoReturn
from urllib import request
@@ -20,14 +22,14 @@ from github import Github
def get_repo_tag_names() -> list[str]:
git_return = os.popen("git tag -l").read()
git_return = subprocess.check_output("git tag -l", text=True)
tag_names = [tag_name for tag_name in git_return.split("\n") if tag_name]
print(f"{len(tag_names)} tag_names found in repo")
return tag_names
def request_repo_info(url: str):
req = request.Request(url)
req = request.Request(url)
resp = request.urlopen(req)
if not resp.status == 200:
print(f"[ERROR] could not contact server: {url}")
@@ -78,15 +80,21 @@ def get_release_zip(tag: str) -> tuple[str, str]:
raise Exception(f"Couldn't find the release matching '{python_version}' and '{TARGET_OS}' in tag '{tag}'.")
def run(command: str) -> None:
subprocess.check_output(command)
start = datetime.datetime.now()
URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender"
URL_BLENDER_CMAKE = "https://raw.githubusercontent.com/blender/blender/{}/build_files/cmake/Modules/FindPythonLibsUnix.cmake"
RE_BLENDER_VERSION_MIN_MAJ = r"Latest Version.+<span>Blender (\d+\.\d+)\..+</span>"
RE_BLENDER_VERSION_MIN_MAJ_PAT = r"Latest Version.+<span>Blender (\d+\.\d+\.\d+)</span>"
URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender"
URL_BLENDER_CMAKE = (
"https://raw.githubusercontent.com/blender/blender/{}/build_files/cmake/Modules/FindPythonLibsUnix.cmake"
)
RE_BLENDER_VERSION_MIN_MAJ = r"Latest Version.+<span>Blender (\d+\.\d+)\..+</span>"
RE_BLENDER_VERSION_MIN_MAJ_PAT = r"Latest Version.+<span>Blender (\d+\.\d+\.\d+)</span>"
RE_BLENDER_PYTHON_VERSION_MAJ_MIN = r"\(_PYTHON_VERSION_SUPPORTED (\d+\.\d+)\)"
BLENDERBIM_DIR = pathlib.Path("/home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/")
BLENDERBIM_DIR = pathlib.Path("/home/runner/work/IfcOpenShell/IfcOpenShell/choco/bonsai/")
print("_____ check choco release needed?")
@@ -97,7 +105,7 @@ should_release = False
target_release_tag = ""
TARGET_OS = "windows-x64"
git_status = os.popen("git status").read()
git_status = subprocess.check_output("git status", text=True)
print(git_status)
for tag_name in get_repo_tag_names():
@@ -143,11 +151,11 @@ print(f"{blender_python_version_maj_min=}")
python_version = f"py{found[0].replace('.', '')}"
print(f"{python_version=}")
blenderbim_build_version = target_release_tag.replace("blenderbim-", "")
blenderbim_build_version = target_release_tag.replace("bonsai-", "")
# url_blenderbim_py3x_win_zip
release_zip_file_name, url_blenderbim_py3x_win_zip = get_release_zip(target_release_tag)
os.popen(f"wget {url_blenderbim_py3x_win_zip} --no-verbose").read()
subprocess.check_call(f"wget {url_blenderbim_py3x_win_zip} --no-verbose")
# sha256sum_blenderbim_py310_win_zip
sha256sum_blenderbim_py3x_win_zip = get_file_sha256_hash(release_zip_file_name)
@@ -161,15 +169,15 @@ topics = {
"path": HERE_DIR / "blenderbim.nuspec",
"key_values": {
"latest_blender_version_maj_min_pat": latest_blender_release_maj_min_pat,
"blenderbim_build_version" : blenderbim_build_version,
"blenderbim_build_version": blenderbim_build_version,
},
},
"install": {
"path": HERE_DIR / "tools" / "chocolateyinstall.ps1",
"key_values": {
"url_blenderbim_py3x_win_zip" : url_blenderbim_py3x_win_zip,
"url_blenderbim_py3x_win_zip": url_blenderbim_py3x_win_zip,
"sha256sum_blenderbim_py3x_win_zip": sha256sum_blenderbim_py3x_win_zip,
"latest_blender_version_maj_min" : blender_version_min_maj,
"latest_blender_version_maj_min": blender_version_min_maj,
},
},
"uninstall": {
@@ -201,13 +209,13 @@ print("[INFO] inserting dynamic chocolatey package parameters successful")
print("\n_____ build choco.exe with mono")
choco_version = "1.1.0"
os.popen(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet").read()
os.popen(f"tar -xzf {choco_version}.tar.gz").read()
run(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet")
run(f"tar -xzf {choco_version}.tar.gz")
print("choco tar unpack successful")
os.chdir("choco-1.1.0")
os.popen("./build.sh").read()
run("./build.sh")
os.popen("cp -r build_output/chocolatey /opt/chocolatey").read()
run("cp -r build_output/chocolatey /opt/chocolatey")
os.chdir(BLENDERBIM_DIR)
if pathlib.Path("/opt/chocolatey/choco.exe").exists():
@@ -215,11 +223,15 @@ if pathlib.Path("/opt/chocolatey/choco.exe").exists():
print("\n_____ build choco pack")
os.popen("mono /opt/chocolatey/choco.exe pack --allow-unofficial").read()
os.popen('mono /opt/chocolatey/choco.exe setapikey --key="{choco_token}" --source="https://push.chocolatey.org/" --allow-unofficial').read()
run("mono /opt/chocolatey/choco.exe pack --allow-unofficial")
run(
'mono /opt/chocolatey/choco.exe setapikey --key="{choco_token}" --source="https://push.chocolatey.org/" --allow-unofficial'
)
print("\n_____ build choco push")
os.popen('mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose').read()
run(
'mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose'
)
print(f"choco push of version: {target_release_tag} successful!")
print(f"it took: {datetime.datetime.now() - start}")
+258 -191
View File
@@ -18,28 +18,28 @@
################################################################################
cmake_minimum_required(VERSION 3.21)
if(NOT DEFINED CMAKE_CXX_STANDARD)
if (NOT DEFINED CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
endif()
if(CMAKE_CXX_STANDARD LESS 17)
if (CMAKE_CXX_STANDARD LESS 17)
message(FATAL_ERROR "C++17 or newer is required.")
endif()
set(CMAKE_CXX_STANDARD_REQUIRED ON) # not necessary, but encouraged
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
if(VERSION_OVERRIDE)
file(READ "../VERSION" "RELEASE_VERSION_")
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
message(STATUS "Detected version '${RELEASE_VERSION}'")
else()
set(RELEASE_VERSION "0.8.0")
endif()
# The VERSION file in the repository root is the single source of truth for the
# release version. Read it unconditionally so a plain source build reports the
# real version through buildinfo.cpp instead of the stale hardcoded 0.8.0
# fallback (see #8164). VERSION_OVERRIDE still controls the branch name used
# when ADD_COMMIT_SHA embeds a commit sha.
file(READ "../VERSION" "RELEASE_VERSION_")
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
message(STATUS "Detected version '${RELEASE_VERSION}'")
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
if(POLICY CMP0141) # 3.25+
# Has to be set before `project` to take effect.
cmake_policy(SET CMP0141 NEW) # Support for `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT`.
cmake_policy(SET CMP0141 NEW) # Support for `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT`.
endif()
if(POLICY CMP0144) # 3.27
cmake_policy(SET CMP0144 NEW) # find_package() uses upper-case <PACKAGENAME>_ROOT variables.
@@ -48,8 +48,6 @@ if(POLICY CMP0167) # 3.30
cmake_policy(SET CMP0167 OLD)
endif()
project(IfcOpenShell VERSION ${RELEASE_VERSION})
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release")
endif()
@@ -65,67 +63,100 @@ endif()
option(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
option(WASM_BUILD "Build a WebAssembly binary." OFF)
option(
ENABLE_BUILD_OPTIMIZATIONS
"Enable certain compiler and linker optimizations on RelWithDebInfo and Release builds."
OFF
)
option(BUILD_SHARED_LIBS "Build IfcParse and IfcGeom as shared libs (SO/DLL)." OFF)
option(ENABLE_BUILD_OPTIMIZATIONS "Enable certain compiler and linker optimizations on RelWithDebInfo and Release builds." OFF)
option(BUILD_SHARED_LIBS "Build IfcOpenShell as shared libraries (required)." ON)
option(MSVC_PARALLEL_BUILD "Multi-threaded compilation in Microsoft Visual Studio (/MP)" OFF)
option(USE_VLD "Use Visual Leak Detector for debugging memory leaks, MSVC-only." OFF)
option(USE_MMAP "Adds a command line options to parse IFC files from memory mapped files using Boost.Iostreams" OFF)
option(NO_WARN "Disable all warnings" OFF)
option(CREATE_BUNDLE "Copy .so files and don't create RPATHS or SOVERSION symlinks" )
option(BUILD_IFCGEOM "Build IfcGeom." ON)
option(BUILD_IFCPYTHON "Build IfcPython." ON)
option(BUILD_IFCPARSE_EXPERIMENTAL_WRAPPER "Build the experimental Clang-generated ifcparse Python wrapper." OFF)
option(BUILD_CONVERT "Build IfcConvert executable." ON)
option(BUILD_DOCUMENTATION "Build IfcOpenShell Documentation." OFF)
option(BUILD_EXAMPLES "Build example applications." ON)
option(BUILD_GEOMSERVER "Build IfcGeomServer executable (Open CASCADE is required)." ON)
option(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
option(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF) # QtViewer requires Qt6
option(BUILD_IFCMODEL_UI "Build minimal Qt IFC model UI prototype" OFF)
option(BUILD_BONSAIVIEWER "Build Bonsai Viewer" OFF) # Requires Qt6 + OpenGL 4.5
option(BUILD_IFCOPENSHELL_PARSE_TESTS "Build C++ unit tests for IfcParse (fetches Catch2 v3)" OFF)
option(BUILD_IFCOPENSHELL_GEOMETRY_TESTS "Build C++ unit tests for IfcGeom (fetches Catch2 v3)" OFF)
option(BUILD_BONSAIVIEWER_TESTS "Build unit tests for Bonsai Viewer core (fetches Catch2 v3)" OFF)
option(BUILD_BONSAIVIEWER_WGPU "Build the experimental wgpu backend (fetches wgpu-native binary release)" OFF)
# IfcViewer (the GL static lib) now links against IfcViewerWgpu because
# SceneLoader drives the wgpu viewport. Auto-enable the wgpu subproject
# whenever BUILD_BONSAIVIEWER is on so the link target exists.
if(BUILD_BONSAIVIEWER AND NOT BUILD_BONSAIVIEWER_WGPU)
message(STATUS "BUILD_BONSAIVIEWER implies BUILD_BONSAIVIEWER_WGPU "
"(SceneLoader uses ViewportWindow); auto-enabling.")
set(BUILD_BONSAIVIEWER_WGPU ON)
endif()
option(BUILD_PACKAGE "" OFF)
# Most users probably need just common schemas,
# but we're keeping it `OFF` by default to avoid disruption
# (e.g. all Python distribution would need to adapt this option to be set).
option(
BUILD_ONLY_COMMON_SCHEMAS
"Build only common IFC schemas (2x3, 4, 4x3_add2). By default all schemas will be built."
IFCOPENSHELL_DEPLOY_QT_RUNTIME
"Deploy Qt runtime dependencies for installed Qt applications."
ON
)
option(
IFCOPENSHELL_DEPLOY_QT_TRANSLATIONS
"Deploy Qt translation catalogs with installed Qt applications."
OFF
)
option(SCHEMA_VERSIONS "Explicitly specify schemas to build." "")
option(WITH_OPENCASCADE "Enable geometry interpretation using Open CASCADE" ON)
option(WITH_CGAL "Enable geometry interpretation using CGAL" ON)
option(WITH_MANIFOLD "Enable geometry interpretation using Manifold" OFF)
option(COLLADA_SUPPORT "Build IfcConvert with COLLADA support (requires OpenCOLLADA)." ON)
option(GLTF_SUPPORT "Build IfcConvert with glTF support (requires json.hpp)." OFF)
option(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
option(WITH_PROJ "Enable output of Earth-Centered Earth-Fixed glTF output using the PROJ library" OFF)
option(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
option(USD_SUPPORT "Build IfcConvert with USD support (requires pixar's USD library)." OFF)
option(WITH_RELATIONSHIP_VALIDATION "Build IfcConvert with option to validate geometrical relationships." OFF)
option(WITH_ROCKSDB "Support a RocksDB key-value store as a file backend in IfcOpenShell" OFF)
option(WITH_ZSTD "Use Zstd compression in RocksDB writes" OFF)
option(USERSPACE_PYTHON_PREFIX "Installs IfcPython for the current user only instead of system-wide." OFF)
option(USE_DEBUG_PYTHON "Use debug binaries when building Debug IfcPython on Windows." OFF)
option(ADD_COMMIT_SHA "Add commit sha and branch in version number, requires git" OFF)
option(
VERSION_OVERRIDE
"Override the version defined in buildinfo.cpp with the file VERSION in the repository root"
OFF
)
option(USE_CCACHE "Enable use of ccache if it's available from PATH." ON)
option(VERSION_OVERRIDE "Override the version defined in buildinfo.cpp with the file VERSION in the repository root" OFF)
set(PYTHON_MODULE_INSTALL_DIR
""
CACHE PATH
set(
PYTHON_MODULE_INSTALL_DIR
"" CACHE PATH
"Directory to install IfcPython package to. By default package is installed in found Python's site-packages."
)
if (VERSION_OVERRIDE)
file(READ "../VERSION" "RELEASE_VERSION_")
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
message(STATUS "Detected version '${RELEASE_VERSION}'")
else()
set(RELEASE_VERSION "0.8.0")
endif()
project(IfcOpenShell VERSION ${RELEASE_VERSION})
# Make sure CMake modules in this project are found first
list(PREPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR})
# Catch2 is fetched only when an explicit C++ test family is enabled, so the
# default build remains offline-capable.
if(BUILD_IFCOPENSHELL_PARSE_TESTS OR BUILD_IFCOPENSHELL_GEOMETRY_TESTS OR BUILD_BONSAIVIEWER_TESTS)
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.4
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(Catch2)
list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras)
include(CTest)
include(Catch)
enable_testing()
endif()
if(MINIMAL_BUILD)
message(STATUS "Setting options for minimal build")
set(BUILD_GEOMSERVER OFF)
@@ -133,18 +164,16 @@ if(MINIMAL_BUILD)
set(WITH_CGAL OFF)
set(COLLADA_SUPPORT OFF)
set(GLTF_SUPPORT OFF)
set(HDF5_SUPPORT OFF)
set(IFCXML_SUPPORT OFF)
set(USD_SUPPORT OFF)
endif()
if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND (NOT BUILD_IFCGEOM))
if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM))
message(STATUS "'IfcGeom' is required with current outputs")
set(BUILD_IFCGEOM ON)
endif()
find_program(CCACHE_FOUND ccache)
if(USE_CCACHE AND CCACHE_FOUND)
if(CCACHE_FOUND)
message(STATUS "`ccache` is found, using it as a compiler launcher.")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_FOUND}")
if(MSVC)
@@ -153,15 +182,17 @@ if(USE_CCACHE AND CCACHE_FOUND)
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>")
# Not needed for Ninja.
if(CMAKE_GENERATOR MATCHES "Visual Studio")
file(COPY_FILE ${CCACHE_FOUND} ${CMAKE_BINARY_DIR}/cl.exe ONLY_IF_DIFFERENT)
set(CMAKE_VS_GLOBALS "CLToolExe=cl.exe" "CLToolPath=${CMAKE_BINARY_DIR}" "UseMultiToolTask=true")
file(COPY_FILE
${CCACHE_FOUND} ${CMAKE_BINARY_DIR}/cl.exe
ONLY_IF_DIFFERENT)
set(CMAKE_VS_GLOBALS
"CLToolExe=cl.exe"
"CLToolPath=${CMAKE_BINARY_DIR}"
"UseMultiToolTask=true"
)
endif()
endif()
endif()
mark_as_advanced(CCACHE_FOUND)
# Variable to accumulate swig definitions from various submodules.
set(SWIG_DEFINES "")
if(MSVC AND MSVC_PARALLEL_BUILD)
add_definitions("/MP")
@@ -179,26 +210,23 @@ include(GNUInstallDirs)
set(IFCOPENSHELL_EXPORT_TARGETS "${PROJECT_NAME}Targets")
# On Windows Release and Debug binaries are not compatible.
# So we add a postfix to avoid issues and allow release and debug installations to coexist.
if(WIN32)
set(CMAKE_DEBUG_POSTFIX "_d")
if(NOT INCLUDEDIR)
set(INCLUDEDIR include)
endif()
if(NOT IS_ABSOLUTE ${INCLUDEDIR})
set(INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR})
endif()
message(STATUS "INCLUDEDIR: ${INCLUDEDIR}")
if(BUILD_SHARED_LIBS)
add_definitions(-DIFC_SHARED_BUILD)
if(MSVC)
message(
WARNING
"Building DLLs against the static VC run-time. This is not recommended if the DLLs are to be redistributed."
)
# C4521: 'identifier' : class 'type' needs to have dll-interface to be used by clients of class 'type2'
# There will be couple hundreds of these so suppress them away, https://msdn.microsoft.com/en-us/library/esew7y1w.aspx
add_definitions(-wd4251)
endif()
if(MSVC)
message(WARNING "Building DLLs against the static VC run-time. This is not recommended if the DLLs are to be redistributed.")
# C4521: 'identifier' : class 'type' needs to have dll-interface to be used by clients of class 'type2'
# There will be couple hundreds of these so suppress them away, https://msdn.microsoft.com/en-us/library/esew7y1w.aspx
add_definitions(-wd4251)
endif()
UNIFY_ENVVARS_AND_CACHE(BOOST_ROOT)
UNIFY_ENVVARS_AND_CACHE(BOOST_LIBRARYDIR)
if(NOT MINIMAL_BUILD)
UNIFY_ENVVARS_AND_CACHE(PYTHON_INCLUDE_DIR)
@@ -214,63 +242,82 @@ foreach(option_flag IN LISTS option_flags)
convert_env_var_to_bool("${option_flag}")
endforeach()
if(WITH_CGAL)
find_package(CGAL REQUIRED)
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_CGAL)
list(APPEND GEOMETRY_KERNELS cgal)
endif()
if(BUILD_IFCGEOM AND WITH_OPENCASCADE)
find_package(OpenCASCADE REQUIRED)
add_definitions(-DIFOPSH_WITH_OPENCASCADE)
# Map OpenCASCADE_LIBRARIES variable from OpenCASCADEConfig.cmake to OpenCASCADE_LIBRARIES used by kernel generic cmake file
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES})
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_OPENCASCADE)
list(APPEND GEOMETRY_KERNELS opencascade)
endif()
set(GLTF_LIBRARIES "")
message(STATUS "BUILD_IFCGEOM WITH_MANIFOLD: ${BUILD_IFCGEOM} ${WITH_MANIFOLD}")
if(BUILD_IFCGEOM AND WITH_MANIFOLD)
find_package(manifold CONFIG REQUIRED)
if(TARGET manifold::manifold)
set(MANIFOLD_LIBRARIES manifold::manifold)
elseif(TARGET manifold)
set(MANIFOLD_LIBRARIES manifold)
else()
message(FATAL_ERROR "Unable to determine manifold target")
endif()
list(APPEND GEOMETRY_KERNELS manifold)
endif()
if(BUILD_IFCGEOM)
list(APPEND GEOMETRY_KERNELS passthrough)
endif()
if(GLTF_SUPPORT)
find_package(nlohmann_json REQUIRED)
set(GLTF_LIBRARIES nlohmann_json::nlohmann_json)
add_definitions(-DWITH_GLTF)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_GLTF)
endif()
# Add USD support to serializers
set(USD_LIBRARIES "")
if(USD_SUPPORT)
find_package(USD REQUIRED)
set(USD_LIBRARIES pxr::USD)
endif(USD_SUPPORT)
set(ROCKSDB_LIBRARIES "")
if(WITH_ROCKSDB)
if (WITH_ROCKSDB)
# Temporaily mess with CMAKE_FIND_PACKAGE_PREFER_CONFIG to help RocksDB
# find it's zstd dependency on Windows.
# Only do it on Windows, otherwise it might create problems as
# findzstd and zstd-config target names do not match.
# https://github.com/facebook/rocksdb/pull/13975
if(WIN32)
set(TEMP CMAKE_FIND_PACKAGE_PREFER_CONFIG)
set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE)
endif()
find_package(RocksDB CONFIG REQUIRED)
mark_as_advanced(RocksDB_DIR)
if(WIN32)
set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ${TEMP})
endif()
message(STATUS "RocksDB: found at '${RocksDB_DIR}'.")
add_library(IFCOPENSHELL_RocksDB INTERFACE)
set(IFCOPENSHELL_ROCKSDB_TARGET IFCOPENSHELL_RocksDB)
set(ROCKSDB_LIBRARIES "IFCOPENSHELL_RocksDB")
target_compile_definitions(IFCOPENSHELL_RocksDB INTERFACE IFOPSH_WITH_ROCKSDB)
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_ROCKSDB)
target_link_libraries(
IFCOPENSHELL_RocksDB
INTERFACE $<IF:$<TARGET_EXISTS:RocksDB::rocksdb-shared>,RocksDB::rocksdb-shared,RocksDB::rocksdb>
)
# See https://github.com/facebook/rocksdb/issues/981.
if(TARGET RocksDB::rocksdb)
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb)
elseif(TARGET RocksDB::rocksdb-shared)
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb-shared)
else()
message(FATAL_ERROR "RocksDB found but neither RocksDB::rocksdb nor RocksDB::rocksdb-shared target exists")
endif()
if(WITH_ZSTD)
if (WITH_ZSTD)
# @todo do we actually need the zstd include dir or rather just pass
# the libzstd.a along with the rocksdb library when needed and feature
# detect based on rocksdb API?
find_package(zstd CONFIG REQUIRED)
mark_as_advanced(zstd_DIR)
message(STATUS "zstd: found at '${zstd_DIR}'.")
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE zstd::libzstd_static)
endif()
install(TARGETS IFCOPENSHELL_RocksDB EXPORT ${IFCOPENSHELL_EXPORT_TARGETS})
@@ -278,7 +325,7 @@ endif()
# Find Boost: On win32 the (hardcoded) default is to use static libraries and
# runtime, when doing running conda-build we pick what conda prepared for us.
if(WIN32 AND NOT DEFINED ENV{CONDA_BUILD})
if(WIN32 AND("$ENV{CONDA_BUILD}" STREQUAL ""))
set(Boost_USE_STATIC_LIBS ON)
set(Boost_USE_STATIC_RUNTIME OFF)
set(Boost_USE_MULTITHREADED ON)
@@ -309,14 +356,8 @@ if(WASM_BUILD)
else()
# @todo review this, shouldn't this be all possible header-only now?
# ... or rewritten using C++17 features?
set(BOOST_COMPONENTS
system
program_options
regex
thread
date_time
iostreams
)
# set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
set(BOOST_COMPONENTS program_options regex thread date_time iostreams)
endif()
if(USE_MMAP)
@@ -326,17 +367,6 @@ if(USE_MMAP)
else()
set(BOOST_COMPONENTS ${BOOST_COMPONENTS} iostreams)
endif()
add_definitions(-DUSE_MMAP)
endif()
# Handle CGAL after Boost settings are set, since CGAL will use them too.
# Do `find_package(Boost)` only after this, to make sure `FindBoost` finds correct components.
# Otherwise it will find components needed for CGAL and we might some libraries.
if(WITH_CGAL)
find_package(CGAL REQUIRED)
set(CGAL_LIBRARIES IFCOPENSHELL_CGAL)
list(APPEND GEOMETRY_KERNELS cgal)
endif()
find_package(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS})
@@ -345,17 +375,8 @@ message(STATUS "Boost libraries found in ${Boost_LIBRARY_DIRS}")
if(COLLADA_SUPPORT)
find_package(OpenCOLLADA REQUIRED)
add_definitions(-DWITH_OPENCOLLADA)
endif()
if(HDF5_SUPPORT)
find_package(HDF5 REQUIRED COMPONENTS C CXX)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} hdf5::hdf5_cpp)
add_definitions(-DWITH_HDF5)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_HDF5)
endif(HDF5_SUPPORT)
if(ENABLE_BUILD_OPTIMIZATIONS)
if(MSVC)
# NOTE: RelWithDebInfo and Release use O2 (= /Ox /Gl /Gy/ = Og /Oi /Ot /Oy /Ob2 /Gs /GF /Gy) by default,
@@ -422,7 +443,7 @@ if(MSVC)
endif()
# Enforce standards-conformance on VS > 2015, older Boost versions fail to compile with this
if(MSVC_VERSION GREATER 1900 AND (Boost_MAJOR_VERSION GREATER 1 OR Boost_MINOR_VERSION GREATER 66))
if(MSVC_VERSION GREATER 1900 AND(Boost_MAJOR_VERSION GREATER 1 OR Boost_MINOR_VERSION GREATER 66))
add_definitions(-permissive-)
endif()
@@ -441,11 +462,11 @@ if(MSVC)
# endforeach()
# endif()
add_definitions(-D_ENABLE_EXTENDED_ALIGNED_STORAGE)
# See #5158.
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.40)
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
endif()
add_definitions(-D_ENABLE_EXTENDED_ALIGNED_STORAGE)
# See #5158.
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.40)
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
endif()
else()
add_definitions(-Wall -Wextra)
@@ -455,10 +476,7 @@ else()
add_definitions(-Wno-maybe-uninitialized)
endif()
if(
CMAKE_CXX_COMPILER_ID MATCHES "GNU"
AND (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 9.0 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 9.0)
)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 9.0 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 9.0))
# OpenCascade spews a lot of deprecated-copy warnings
add_definitions(-Wno-deprecated-copy)
endif()
@@ -469,45 +487,28 @@ else()
endif()
endif(MSVC)
include_directories(${OPENCOLLADA_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS} ${HDF5_INCLUDE_DIR})
include_directories(${INCLUDE_DIRECTORIES}
${Boost_INCLUDE_DIRS}
${CGAL_INCLUDE_DIR} ${GMP_INCLUDE_DIR} ${MPFR_INCLUDE_DIR}
)
if(NOT SCHEMA_VERSIONS)
# `WASM_BUILD` - super arbitrarily try to keep size down at least a little bit
if(BUILD_ONLY_COMMON_SCHEMAS OR WASM_BUILD)
if(WASM_BUILD)
# super arbitrarily try to keep size down at least a little bit
set(SCHEMA_VERSIONS "2x3" "4" "4x3_add2")
else()
set(SCHEMA_VERSIONS
"2x3"
"4"
"4x1"
"4x2"
"4x3"
"4x3_tc1"
"4x3_add1"
"4x3_add2"
)
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3" "4x3_tc1" "4x3_add1" "4x3_add2")
endif()
endif()
message(STATUS "IFC SCHEMA_VERSIONS that will be used for the build: ${SCHEMA_VERSIONS}.")
set(SCHEMA_DEFINITIONS "")
foreach(schema ${SCHEMA_VERSIONS})
list(APPEND SCHEMA_DEFINITIONS "-DHAS_SCHEMA_${schema}")
endforeach()
string(REPLACE ";" ")(" schema_version_seq "(${SCHEMA_VERSIONS})")
list(APPEND SCHEMA_DEFINITIONS "-DSCHEMA_SEQ=${schema_version_seq}")
if(COMPILE_SCHEMA)
# @todo, this appears to be untested at the moment
find_package(PythonInterp)
if(NOT PYTHONINTERP_FOUND)
message(
FATAL_ERROR
"A Python interpreter is necessary when COMPILE_SCHEMA is enabled. Disable COMPILE_SCHEMA or fix Python paths to proceed."
)
message(FATAL_ERROR "A Python interpreter is necessary when COMPILE_SCHEMA is enabled. Disable COMPILE_SCHEMA or fix Python paths to proceed.")
endif()
set(IFC_RELEASE_NOT_USED ${SCHEMA_VERSIONS})
@@ -527,10 +528,7 @@ if(COMPILE_SCHEMA)
if("${PYPARSING_FOUND}" STREQUAL "-1")
message(STATUS "Installing pyparsing")
execute_process(
COMMAND ${PYTHON_EXECUTABLE} -m pip "install" --user pyparsing
RESULT_VARIABLE SUCCESS
)
execute_process(COMMAND ${PYTHON_EXECUTABLE} -m pip "install" --user pyparsing RESULT_VARIABLE SUCCESS)
if(NOT "${SUCCESS}" STREQUAL "0")
execute_process(COMMAND pip "install" --user pyparsing RESULT_VARIABLE SUCCESS)
@@ -546,11 +544,10 @@ if(COMPILE_SCHEMA)
# Bootstrap the parser
message(STATUS "Compiling schema, this will take a while...")
execute_process(
COMMAND ${PYTHON_EXECUTABLE} bootstrap.py express.bnf
WORKING_DIRECTORY ../src/ifcexpressparser
COMMAND ${PYTHON_EXECUTABLE} bootstrap.py
WORKING_DIRECTORY ../src/ifcopenshell-python/ifcopenshell/express
OUTPUT_FILE express_parser.py
RESULT_VARIABLE SUCCESS
)
RESULT_VARIABLE SUCCESS)
if(NOT "${SUCCESS}" STREQUAL "0")
message(FATAL_ERROR "Failed to bootstrap parser. Make sure pyparsing is installed")
@@ -558,10 +555,9 @@ if(COMPILE_SCHEMA)
# Generate code
execute_process(
COMMAND ${PYTHON_EXECUTABLE} ../ifcexpressparser/express_parser.py ../../${COMPILE_SCHEMA}
COMMAND ${PYTHON_EXECUTABLE} ../ifcopenshell-python/ifcopenshell/express/express_parser.py ../../${COMPILE_SCHEMA}
WORKING_DIRECTORY ../src/ifcparse
OUTPUT_VARIABLE COMPILED_SCHEMA_NAME
)
OUTPUT_VARIABLE COMPILED_SCHEMA_NAME)
# Prevent the schema that had just been compiled from being excluded
foreach(schema ${SCHEMA_VERSIONS})
@@ -576,17 +572,52 @@ if(NOT Boost_VERSION LESS 105800)
add_definitions(-DBOOST_OPTIONAL_USE_OLD_DEFINITION_OF_NONE)
endif()
add_subdirectory(../src/plugin plugin)
add_subdirectory(../src/ifcparse ifcparse)
set(IFCOPENSHELL_LIBRARIES IfcParse)
if(BUILD_IFCOPENSHELL_PARSE_TESTS)
add_subdirectory(../src/ifcparse/tests ifcparse/tests)
endif()
if(BUILD_EXAMPLES OR BUILD_BONSAIVIEWER)
add_subdirectory(../src/helpers helpers)
endif()
if(BUILD_IFCPARSE_EXPERIMENTAL_WRAPPER)
add_subdirectory(../src/wrappergen wrappergen)
endif()
if(BUILD_IFCGEOM)
# CGAL::CGAL target already has dependencies resolved.
if(WITH_CGAL AND CGAL_DIR)
set(CGAL_LIBRARIES CGAL::CGAL)
message(STATUS "Using found CGAL package at '${CGAL_DIR}'")
elseif(WITH_CGAL AND NOT CGAL_DIR)
find_library(libGMP NAMES gmp mpir PATHS ${GMP_LIBRARY_DIR} NO_DEFAULT_PATH)
find_library(libMPFR NAMES mpfr PATHS ${MPFR_LIBRARY_DIR} NO_DEFAULT_PATH)
if(NOT libGMP)
message(FATAL_ERROR "Unable to find GMP library files, aborting")
endif()
if(NOT libMPFR)
message(FATAL_ERROR "Unable to find MPFR library files, aborting")
endif()
list(APPEND CGAL_LIBRARIES "${libMPFR}")
list(APPEND CGAL_LIBRARIES "${libGMP}")
endif()
add_subdirectory(../src/ifcgeom ifcgeom)
if(BUILD_IFCOPENSHELL_GEOMETRY_TESTS)
add_subdirectory(../src/ifcgeom/tests ifcgeom/tests)
endif()
elseif(BUILD_IFCOPENSHELL_GEOMETRY_TESTS)
message(FATAL_ERROR "BUILD_IFCOPENSHELL_GEOMETRY_TESTS requires BUILD_IFCGEOM=ON.")
endif(BUILD_IFCGEOM)
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
if(BUILD_CONVERT OR BUILD_IFCPYTHON OR BUILD_BONSAIVIEWER)
add_subdirectory(../src/serializers serializers)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} ${SERIALIZER_SCHEMA_LIBRARIES})
endif(BUILD_CONVERT OR BUILD_IFCPYTHON)
endif(BUILD_CONVERT OR BUILD_IFCPYTHON OR BUILD_BONSAIVIEWER)
if(BUILD_CONVERT)
add_subdirectory(../src/ifcconvert ifcconvert)
@@ -605,8 +636,8 @@ if(ADD_COMMIT_SHA)
endif()
if(GIT_FOUND)
if(VERSION_OVERRIDE)
set(git_branch ${RELEASE_VERSION})
if (VERSION_OVERRIDE)
set (git_branch ${RELEASE_VERSION})
else()
message("git found: ${GIT_EXECUTABLE} with version ${GIT_VERSION_STRING}")
execute_process(
@@ -618,8 +649,8 @@ if(ADD_COMMIT_SHA)
string(REPLACE "\n" ";" git_branch_list "${git_branches}")
foreach(git_branch_candidate IN ITEMS ${git_branch_list})
string(REPLACE "*" "" git_branch_candidate_temp "${git_branch_candidate}")
string(STRIP "${git_branch_candidate_temp}" git_branch_candidate_2)
string(REPLACE "*" "" git_branch_candidate_temp "${git_branch_candidate}")
string(STRIP "${git_branch_candidate_temp}" git_branch_candidate_2)
if(NOT git_branch_candidate_2 MATCHES "^HEAD$")
string(REPLACE "/" ";" git_branch_candidate_2_list "${git_branch_candidate_2}")
list(GET git_branch_candidate_2_list -1 git_branch)
@@ -637,17 +668,22 @@ if(ADD_COMMIT_SHA)
message(STATUS "IfcOpenShell branch: \"${git_branch}\"")
message(STATUS "IfcOpenShell commit: \"${git_sha}\"")
if("${git_branch}" STREQUAL "" OR "${git_sha}" STREQUAL "")
if ("${git_branch}" STREQUAL "" OR "${git_sha}" STREQUAL "")
message(FATAL_ERROR "Unable to determine commit sha and/or branch")
endif()
target_compile_definitions(
IfcParse
PRIVATE -DIFCOPENSHELL_BRANCH=${git_branch} -DIFCOPENSHELL_COMMIT=${git_sha}
target_compile_definitions(IfcParse PRIVATE
-DIFCOPENSHELL_BRANCH=${git_branch}
-DIFCOPENSHELL_COMMIT=${git_sha}
)
endif()
endif(ADD_COMMIT_SHA)
# Always expose the release version (from the VERSION file) to buildinfo.cpp so
# that a build without commit-sha info reports the correct version instead of a
# stale hardcoded fallback. See #8164.
target_compile_definitions(IfcParse PRIVATE IFCOPENSHELL_VERSION_STRING=${RELEASE_VERSION})
if(MSVC)
# @todo still needs to be understood better, but the cgal and cgal-simple kernel cause multiply defined boost lambda placeholders _1 ... _3
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE")
@@ -661,10 +697,6 @@ if(BUILD_DOCUMENTATION)
add_subdirectory(../docs docs)
endif()
if(BUILD_IFCPYTHON)
add_subdirectory(../src/ifcwrap ifcwrap)
endif()
if(BUILD_EXAMPLES)
add_subdirectory(../src/examples examples)
endif()
@@ -677,8 +709,48 @@ if(BUILD_IFCPYTHON AND WITH_CGAL)
add_subdirectory(../src/svgfill svgfill)
endif()
if(BUILD_QTVIEWER)
add_subdirectory(../src/qtviewer qtviewer)
if(BUILD_IFCPYTHON)
add_subdirectory(../src/ifcwrap ifcwrap)
endif()
if(BUILD_IFCMODEL_UI)
add_subdirectory(../src/ifcmodel-ui ifcmodel-ui)
endif()
if(BUILD_IFCGEOM)
# install(FILES ${IFCGEOM_H_FILES}
# DESTINATION ${INCLUDEDIR}/ifcgeom
# )
install(FILES ${SCHEMA_AGNOSTIC_H_FILES}
DESTINATION ${INCLUDEDIR}/ifcgeom
)
file(GLOB SERIALIZATION_H_FILES ../src/ifcgeom/serialization/*.h)
install(FILES ${SERIALIZATION_H_FILES}
DESTINATION ${INCLUDEDIR}/ifcgeom/serialization
)
foreach(kernel ${GEOMETRY_KERNELS})
file(GLOB IFCGEOM_H_FILES ../src/ifcgeom/kernels/${kernel}/*.h)
install(FILES ${IFCGEOM_H_FILES}
DESTINATION ${INCLUDEDIR}/ifcgeom/kernels/${kernel}
)
endforeach()
install(
TARGETS ${IFCGEOM_SCHEMA_LIBRARIES} ${kernel_libraries} IfcGeom
EXPORT ${IFCOPENSHELL_EXPORT_TARGETS}
)
endif(BUILD_IFCGEOM)
if(BUILD_BONSAIVIEWER)
# IfcViewer is the unified scene + render lib since the wgpu/ifcviewer
# merge — wgpu-native is fetched inside its CMakeLists.txt.
add_subdirectory(../src/ifcviewer ifcviewer)
if(BUILD_BONSAIVIEWER_WGPU)
add_subdirectory(../src/ifcviewer-minimal ifcviewer-minimal)
endif()
add_subdirectory(../src/bonsaiviewer bonsaiviewer)
endif()
# Cmake uninstall target
@@ -686,23 +758,23 @@ if(NOT TARGET uninstall)
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake"
IMMEDIATE
@ONLY
)
IMMEDIATE @ONLY)
add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake)
add_custom_target(uninstall
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake)
endif()
# Packaging
list(APPEND CPACK_SOURCE_IGNORE_FILES "/\\\\.git" "/build/" "/.pytest_cache/" "/__pycache__/")
list(APPEND CPACK_SOURCE_IGNORE_FILES
"/\\\\.git"
"/build/"
"/.pytest_cache/"
"/__pycache__/"
)
set(CPACK_SOURCE_INSTALLED_DIRECTORIES "${CMAKE_SOURCE_DIR}/..;/")
set(CPACK_PACKAGE_NAME
"${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}"
)
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}")
set(CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}${EXTRA_VERSION}")
set(CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}-${CMAKE_SYSTEM_NAME}"
)
SET(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}-${CMAKE_SYSTEM_NAME}")
set(CPACK_PACKAGE_DIRECTORY "${PROJECT_BINARY_DIR}/assets")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "IfcOpenShell")
set(CPACK_PACKAGE_DESCRIPTION "IfcOpenShell.")
@@ -715,7 +787,6 @@ set(CPACK_PACKAGE_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
set(CPACK_GENERATOR "TGZ;DEB")
set(CPACK_SOURCE_GENERATOR "TGZ")
set(BOOST_DEPS "")
foreach(COMPONENT IN ITEMS ${BOOST_COMPONENTS})
string(REPLACE "_" "-" COMP ${COMPONENT})
set(BOOST_DEPS "${BOOST_DEPS}, libboost-${COMP}-dev")
@@ -723,16 +794,12 @@ endforeach(COMPONENT)
set(CPACK_DEBIAN_PACKAGE_NAME "${PROJECT_NAME}")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${CPACK_PACKAGE_CONTACT}")
set(CPACK_DEBIAN_PACKAGE_DEPENDS
"python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libhdf5-serial-dev, libpython3-dev, python3-pytest ${BOOST_DEPS}"
)
set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libpython3-dev, python3-pytest ${BOOST_DEPS}")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION_SUMMARY "${CPACK_PACKAGE_DESCRIPTION_SUMMARY}")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "${CPACK_PACKAGE_DESCRIPTION}")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_DEBIAN_PACKAGE_SECTION "science")
set(CPACK_DEBIAN_PACKAGE_VERSION
"${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}${EXTRA_VERSION}"
)
set(CPACK_DEBIAN_PACKAGE_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}${EXTRA_VERSION}")
set(CPACK_DEBIAN_ARCHITECTURE "${CMAKE_SYSTEM_PROCESSOR}")
# set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_SOURCE_DIR}/cmake/debian/postinst")
+1 -4
View File
@@ -15,7 +15,6 @@
"BUILD_CONVERT": "ON",
"BUILD_IFCMAX": "OFF",
"IFCXML_SUPPORT": "ON",
"HDF5_SUPPORT": "ON",
"SCHEMA_VERSIONS": "4x3_add2",
"CMAKE_GENERATOR_PLATFORM": "",
"CMAKE_GENERATOR_TOOLSET": ""
@@ -50,8 +49,6 @@
"MPFR_LIBRARY_DIR": "$env{LIBRARY_PREFIX}/lib",
"Boost_LIBRARY_DIR": "$env{LIBRARY_PREFIX}/lib",
"Boost_INCLUDE_DIR": "$env{LIBRARY_PREFIX}/include",
"HDF5_INCLUDE_DIR": "$env{LIBRARY_PREFIX}/include",
"HDF5_LIBRARY_DIR": "$env{LIBRARY_PREFIX}/lib",
"ZLIB_INCLUDE_DIR": "$env{LIBRARY_PREFIX}/include"
}
},
@@ -110,4 +107,4 @@
}
}
]
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
# - `GMP_LIBRARY_DIR`
# - `MPFR_INCLUDE_DIR`
# - `MPFR_LIBRARY_DIR`
# If input variables are not specified, try to find HDF5 config.
# If input variables are not specified, try to find CGAL config.
# Input variables could also be provided as environment variables.
#
# Output targets:
-109
View File
@@ -1,109 +0,0 @@
#
# Input variables:
# - `HDF5_INCLUDE_DIR`
# - `HDF5_LIBRARY_DIR`
# - `HDF5_LIBRARIES`
# If input variables are not specified, try to find HDF5 config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `HDF5_INCLUDE_DIR`
# - `HDF5_LIBRARY_DIR`
# - `HDF5_LIBRARIES`
#
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARIES)
# To avoid cyclic calls to this file
list(REMOVE_ITEM CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
if(NOT HDF5_INCLUDE_DIR)
message(STATUS "No HDF5 include directory specified")
else()
set(HDF5_INCLUDE_DIR "${HDF5_INCLUDE_DIR}" CACHE FILEPATH "HDF5 header files")
endif()
if(NOT HDF5_LIBRARY_DIR)
message(STATUS "No HDF5 library directory specified")
else()
set(HDF5_LIBRARY_DIR "${HDF5_LIBRARY_DIR}" CACHE FILEPATH "HDF5 library files")
endif()
if(HDF5_LIBRARY_DIR)
# result of the HDF5 ctest package
# Find zlib using cmake find_library. How should this be implemented?
# FIND_LIBRARY(NAMES z libz libz_debug PATHS ... NO_DEFAULT_PATH)
if(NOT DEFINED ENV{CONDA_BUILD})
# result of the HDF5 ctest package
if(WIN32)
set(zlib_post lib)
set(lib_ext lib)
else()
set(lib_ext a)
endif()
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
set(debug_postfix "_debug")
endif()
set(HDF5_LIBRARIES
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
)
else()
message(STATUS "Packaging hdf5 and zlib for conda distribution")
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
# macOS
set(zlib_post libz)
set(lib_ext dylib)
set(HDF5_LIBRARIES
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
)
else()
# linux and windows
# Find HDF5 package
find_package(HDF5 REQUIRED COMPONENTS C CXX)
# Find ZLIB package
find_package(ZLIB REQUIRED)
# Include directories
include_directories(${HDF5_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS})
# Link libraries
set(HDF5_LIBRARIES ${HDF5_LIBRARIES} ${ZLIB_LIBRARIES})
message(STATUS "HDF5 libraries: ${HDF5_LIBRARIES}")
endif()
endif()
endif()
if(NOT HDF5_INCLUDE_DIR OR NOT HDF5_LIBRARY_DIR)
# First try to find it as a config.
find_package(HDF5 CONFIG)
mark_as_advanced(HDF5_DIR)
if(HDF5_DIR)
message(STATUS "HDF5: found config at '${HDF5_DIR}'.")
set(HDF5_LIBRARIES hdf5_cpp-static)
else()
# If it failed, still try to find as a module.
# E.g. on Ubuntu `libhdf5-dev` doesn't provie hdf5-config.cmake.
# Will automatically fill HDF5_LIBRARIES and HDF5_INCLUDE_DIR.
find_package(HDF5 COMPONENTS CXX)
if(NOT HDF5_INCLUDE_DIR)
message(
FATAL_ERROR
"HDF5_INCLUDE_DIR is not provided (current value: '${HDF5_INCLUDE_DIR}'). "
"HDF5_LIBRARY_DIR is not provided (current value: '${HDF5_LIBRARY_DIR}'). "
"Also could not find HDF5 package (neither module or config)."
)
endif()
endif()
endif()
# Restore module path.
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
+131
View File
@@ -0,0 +1,131 @@
# This file was generated with the assistance of an AI coding tool.
################################################################################
# #
# This file is part of IfcOpenShell. #
# #
# IfcOpenShell is free software: you can redistribute it and/or modify #
# it under the terms of the Lesser GNU General Public License as published by #
# the Free Software Foundation, either version 3.0 of the License, or #
# (at your option) any later version. #
# #
# IfcOpenShell is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# Lesser GNU General Public License for more details. #
# #
# You should have received a copy of the Lesser GNU General Public License #
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
# #
################################################################################
include("${CMAKE_CURRENT_LIST_DIR}/utilities.cmake" OPTIONAL)
set(_IfcOpenShell_find_args)
if(IfcOpenShell_FIND_VERSION)
list(APPEND _IfcOpenShell_find_args "${IfcOpenShell_FIND_VERSION}")
if(IfcOpenShell_FIND_VERSION_EXACT)
list(APPEND _IfcOpenShell_find_args EXACT)
endif()
endif()
list(APPEND _IfcOpenShell_find_args CONFIG QUIET)
if(IfcOpenShell_FIND_COMPONENTS)
list(APPEND _IfcOpenShell_find_args COMPONENTS ${IfcOpenShell_FIND_COMPONENTS})
endif()
set(_IfcOpenShell_saved_module_path "${CMAKE_MODULE_PATH}")
list(REMOVE_ITEM CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}")
find_package(IfcOpenShell ${_IfcOpenShell_find_args})
set(CMAKE_MODULE_PATH "${_IfcOpenShell_saved_module_path}")
if(NOT IfcOpenShell_FOUND)
set(_IfcOpenShell_error "Could not find an IfcOpenShell CMake config package. Set IfcOpenShell_DIR or CMAKE_PREFIX_PATH.")
if(IfcOpenShell_FIND_REQUIRED)
message(FATAL_ERROR "${_IfcOpenShell_error}")
elseif(NOT IfcOpenShell_FIND_QUIETLY)
message(STATUS "${_IfcOpenShell_error}")
endif()
return()
endif()
set(_IfcOpenShell_required_targets IfcOpenShell::IfcParse IfcOpenShell::IfcGeom)
set(_IfcOpenShell_missing_targets "")
foreach(_IfcOpenShell_target IN LISTS _IfcOpenShell_required_targets)
if(NOT TARGET ${_IfcOpenShell_target})
list(APPEND _IfcOpenShell_missing_targets ${_IfcOpenShell_target})
endif()
endforeach()
if(_IfcOpenShell_missing_targets)
set(IfcOpenShell_FOUND FALSE)
string(REPLACE ";" ", " _IfcOpenShell_missing_targets_text "${_IfcOpenShell_missing_targets}")
set(_IfcOpenShell_error "IfcOpenShell config was found, but required targets are missing: ${_IfcOpenShell_missing_targets_text}.")
if(IfcOpenShell_FIND_REQUIRED)
message(FATAL_ERROR "${_IfcOpenShell_error}")
elseif(NOT IfcOpenShell_FIND_QUIETLY)
message(STATUS "${_IfcOpenShell_error}")
endif()
return()
endif()
if(NOT DEFINED IFCOPENSHELL_WITH_OPENCASCADE)
set(IFCOPENSHELL_WITH_OPENCASCADE OFF)
if(TARGET IfcOpenShell::geometry_kernel_opencascade)
set(IFCOPENSHELL_WITH_OPENCASCADE ON)
endif()
endif()
if(NOT DEFINED IFCOPENSHELL_WITH_CGAL)
set(IFCOPENSHELL_WITH_CGAL OFF)
if(TARGET IfcOpenShell::IFCOPENSHELL_CGAL)
set(IFCOPENSHELL_WITH_CGAL ON)
endif()
endif()
if(NOT DEFINED IFCOPENSHELL_IFCXML)
set(IFCOPENSHELL_IFCXML OFF)
endif()
if(NOT DEFINED IFCOPENSHELL_WITH_ROCKSDB)
set(IFCOPENSHELL_WITH_ROCKSDB OFF)
endif()
set(IFCOPENSHELL_LIBRARIES IfcOpenShell::IfcParse)
foreach(_IfcOpenShell_target IN ITEMS IfcOpenShell::geometry_serializer IfcOpenShell::Serializers)
if(TARGET ${_IfcOpenShell_target})
list(APPEND IFCOPENSHELL_LIBRARIES ${_IfcOpenShell_target})
endif()
endforeach()
set(IFCOPENSHELL_KERNEL_LIBRARIES "")
foreach(_IfcOpenShell_target IN ITEMS
IfcOpenShell::geometry_kernel_opencascade
IfcOpenShell::geometry_kernel_cgal
IfcOpenShell::geometry_kernel_cgal_simple
)
if(TARGET ${_IfcOpenShell_target})
list(APPEND IFCOPENSHELL_KERNEL_LIBRARIES ${_IfcOpenShell_target})
endif()
endforeach()
set(IFCOPENSHELL_GEOMETRY_LIBRARIES IfcOpenShell::IfcGeom ${IFCOPENSHELL_KERNEL_LIBRARIES})
if(TARGET IfcOpenShell::OpenCASCADE_INTERFACE)
set(OpenCASCADE_LIBRARIES IfcOpenShell::OpenCASCADE_INTERFACE)
endif()
if(TARGET IfcOpenShell::IFCOPENSHELL_CGAL)
set(CGAL_LIBRARIES IfcOpenShell::IFCOPENSHELL_CGAL)
endif()
if(TARGET IfcOpenShell::svgfill)
set(IFCOPENSHELL_SVGFILL_LIBRARY IfcOpenShell::svgfill)
endif()
mark_as_advanced(IfcOpenShell_DIR)
unset(_IfcOpenShell_error)
unset(_IfcOpenShell_find_args)
unset(_IfcOpenShell_missing_targets)
unset(_IfcOpenShell_missing_targets_text)
unset(_IfcOpenShell_required_targets)
unset(_IfcOpenShell_target)
+11
View File
@@ -43,6 +43,17 @@ if(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR)
set_target_properties(TKernel PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${OpenCASCADE_INCLUDE_DIR}")
endif()
if(
OpenCASCADE_VERSION VERSION_LESS "7.9.0"
AND CMAKE_VERSION GREATER_EQUAL "3.24"
AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU"
)
# Before 7.9.0 targets in OCCT cmake configs are not linked to each other
# leading to missing symbols on Unix. Link them as a single group as a workaround.
# Only needed for gcc, because other compilers (e.g. Apple Clang, MSVC) do rescan automatically.
set(OpenCASCADE_LIBRARIES "$<LINK_GROUP:RESCAN,${OpenCASCADE_LIBRARIES}>")
endif()
if(OpenCASCADE_VERSION VERSION_LESS "7.9.0" AND WIN32)
# Bug in OCCT cmake configs < 7.9.0 - missing linked library.
list(APPEND OpenCASCADE_LIBRARIES WSOCK32.lib)
+4 -3
View File
@@ -139,7 +139,8 @@ if(NOT OpenCOLLADA_DIR)
endif()
endif(NOT OpenCOLLADA_DIR)
if(OPENCOLLADA_FOUND)
add_definitions(-DWITH_OPENCOLLADA)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_OPENCOLLADA)
if(OPENCOLLADA_FOUND AND NOT TARGET OpenCOLLADA::OpenCOLLADA)
add_library(OpenCOLLADA::OpenCOLLADA INTERFACE IMPORTED)
target_include_directories(OpenCOLLADA::OpenCOLLADA INTERFACE ${OPENCOLLADA_INCLUDE_DIRS})
target_link_libraries(OpenCOLLADA::OpenCOLLADA INTERFACE ${OPENCOLLADA_LIBRARIES})
endif()
+12 -6
View File
@@ -6,7 +6,7 @@
# Input variables could also be provided as environment variables.
#
# Output targets:
# - `PROJ::proj`
# - `proj::proj`
#
# To avoid cyclic calls to this file
@@ -34,10 +34,12 @@ if((NOT PROJ_INCLUDE_DIR AND NOT PROJ_LIBRARIES))
message(FATAL_ERROR "Unable to find PROJ libraries in: ${PROJ_LIBRARY_DIR}")
endif()
add_library(PROJ::proj INTERFACE IMPORTED)
target_include_directories(PROJ::proj INTERFACE "${PROJ_INCLUDE_DIR}")
target_link_libraries(PROJ::proj INTERFACE ${PROJ_LIBRARIES})
target_link_directories(PROJ::proj INTERFACE "${PROJ_LIBRARY}")
if(NOT TARGET proj::proj)
add_library(proj::proj INTERFACE IMPORTED)
target_include_directories(proj::proj INTERFACE "${PROJ_INCLUDE_DIR}")
target_link_libraries(proj::proj INTERFACE ${PROJ_LIBRARIES})
target_link_directories(proj::proj INTERFACE "${PROJ_LIBRARY}")
endif()
endif()
else()
find_library(PROJ_LIBRARY NAMES proj PATHS ${PROJ_LIBRARY_DIR})
@@ -50,7 +52,11 @@ else()
set(PROJ_INCLUDE_DIR ${PROJ_INCLUDE_DIR} CACHE FILEPATH "PROJ header files")
message(STATUS "Looking for PROJ include files in: ${PROJ_INCLUDE_DIR}")
include_directories(${PROJ_INCLUDE_DIR})
if(NOT TARGET proj::proj)
add_library(proj::proj INTERFACE IMPORTED)
target_include_directories(proj::proj INTERFACE "${PROJ_INCLUDE_DIR}")
target_link_libraries(proj::proj INTERFACE ${PROJ_LIBRARIES})
endif()
endif()
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
-3
View File
@@ -64,7 +64,6 @@ set(USD_LIBRARIES
find_library(USD_LIBRARY NAMES ${USD_LIBRARIES} PATHS ${USD_LIBRARY_DIR})
if(USD_LIBRARY)
message(STATUS "USD libraries ${USD_LIBRARIES} found in: ${USD_LIBRARY_DIR}")
link_directories(${USD_LIBRARY_DIR})
else()
message(FATAL_ERROR "Unable to find USD libraries in: ${USD_LIBRARY_DIR}")
endif()
@@ -82,5 +81,3 @@ if(MSVC)
endif()
target_compile_definitions(pxr::USD INTERFACE PXR_STATIC WITH_USD)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_USD)
+40 -4
View File
@@ -1,5 +1,7 @@
@PACKAGE_INIT@
set_and_check(IFCOPENSHELL_LIBRARY_DIR "@PACKAGE_CMAKE_INSTALL_LIBDIR@")
# Variable to inspect installed schema versions.
set(IFCOPENSHELL_SCHEMA_VERSIONS @SCHEMA_VERSIONS@)
@@ -7,12 +9,26 @@ set(IFCOPENSHELL_WITH_OPENCASCADE @WITH_OPENCASCADE@)
set(IFCOPENSHELL_WITH_CGAL @WITH_CGAL@)
set(IFCOPENSHELL_IFCXML @IFCXML_SUPPORT@)
set(IFCOPENSHELL_WITH_ROCKSDB @WITH_ROCKSDB@)
set(IFCOPENSHELL_COLLADA_SUPPORT @COLLADA_SUPPORT@)
set(IFCOPENSHELL_GLTF_SUPPORT @GLTF_SUPPORT@)
set(IFCOPENSHELL_HDF5_SUPPORT @HDF5_SUPPORT@)
set(IFCOPENSHELL_WITH_PROJ @WITH_PROJ@)
set(IFCOPENSHELL_USD_SUPPORT @USD_SUPPORT@)
include(CMakeFindDependencyMacro)
set(Boost_USE_STATIC_LIBS ON)
set(Boost_USE_STATIC_RUNTIME OFF)
set(Boost_USE_MULTITHREADED ON)
set(IFCOPENSHELL_BOOST_USE_STATIC_LIBS "@Boost_USE_STATIC_LIBS@")
set(IFCOPENSHELL_BOOST_USE_STATIC_RUNTIME "@Boost_USE_STATIC_RUNTIME@")
set(IFCOPENSHELL_BOOST_USE_MULTITHREADED "@Boost_USE_MULTITHREADED@")
if(NOT "${IFCOPENSHELL_BOOST_USE_STATIC_LIBS}" STREQUAL "")
set(Boost_USE_STATIC_LIBS ${IFCOPENSHELL_BOOST_USE_STATIC_LIBS})
endif()
if(NOT "${IFCOPENSHELL_BOOST_USE_STATIC_RUNTIME}" STREQUAL "")
set(Boost_USE_STATIC_RUNTIME ${IFCOPENSHELL_BOOST_USE_STATIC_RUNTIME})
endif()
if(NOT "${IFCOPENSHELL_BOOST_USE_MULTITHREADED}" STREQUAL "")
set(Boost_USE_MULTITHREADED ${IFCOPENSHELL_BOOST_USE_MULTITHREADED})
endif()
set(Boost_COMPONENTS
system
program_options
@@ -43,13 +59,33 @@ if(IFCOPENSHELL_WITH_ROCKSDB)
endif()
if(IFCOPENSHELL_IFCXML)
find_dependency(LibXml2 CONFIG)
find_dependency(LibXml2)
endif()
if(IFCOPENSHELL_WITH_CGAL)
find_dependency(CGAL CONFIG)
endif()
if(IFCOPENSHELL_COLLADA_SUPPORT)
find_dependency(OpenCOLLADA)
endif()
if(IFCOPENSHELL_GLTF_SUPPORT)
find_dependency(nlohmann_json CONFIG)
endif()
if(IFCOPENSHELL_HDF5_SUPPORT)
find_dependency(HDF5 COMPONENTS C CXX)
endif()
if(IFCOPENSHELL_WITH_PROJ)
find_dependency(PROJ)
endif()
if(IFCOPENSHELL_USD_SUPPORT)
find_dependency(USD)
endif()
if(IFCOPENSHELL_WITH_OPENCASCADE)
find_dependency(OpenCASCADE CONFIG)
if(OpenCASCADE_VERSION VERSION_LESS "7.7.0")
+1
View File
@@ -17,6 +17,7 @@ configure_package_config_file(
${CONFIG_PACKAGE_INPUT}
${CONFIG_PACKAGE_OUTPUT}
INSTALL_DESTINATION ${CONFIG_PACKAGE_LOCATION}
PATH_VARS CMAKE_INSTALL_LIBDIR
)
install(FILES "${CONFIG_PACKAGE_OUTPUT}" "${CONFIG_VERSION_OUTPUT}" DESTINATION ${CONFIG_PACKAGE_LOCATION})
+115
View File
@@ -41,6 +41,121 @@ macro(SET_INSTALL_RPATHS _target _paths)
set_target_properties(${_target} PROPERTIES INSTALL_RPATH "${${_target}_rpaths}")
endmacro()
macro(SET_INSTALL_SELF_RPATH _target)
if(IS_ABSOLUTE "${CMAKE_INSTALL_LIBDIR}")
SET_INSTALL_RPATHS(${_target} "${CMAKE_INSTALL_LIBDIR}")
elseif(APPLE)
SET_INSTALL_RPATHS(${_target} "@loader_path")
else()
SET_INSTALL_RPATHS(${_target} "$ORIGIN")
endif()
endmacro()
function(ifcopenshell_plugin_target TARGET)
set_target_properties(${TARGET} PROPERTIES PREFIX "")
if((NOT WIN32) AND BUILD_SHARED_LIBS AND NOT WASM_BUILD AND NOT CREATE_BUNDLE AND NOT CMAKE_INSTALL_RPATH AND COMMAND SET_INSTALL_SELF_RPATH)
SET_INSTALL_SELF_RPATH(${TARGET})
endif()
endfunction()
function(ifcopenshell_wasm_plugin_link_options TARGET REGISTRATION_SYMBOL)
ifcopenshell_plugin_target(${TARGET})
if(NOT WASM_BUILD)
return()
endif()
cmake_parse_arguments(PLUGIN "" "OPTIMIZATION" "" ${ARGN})
if(NOT PLUGIN_OPTIMIZATION)
set(PLUGIN_OPTIMIZATION -O1)
endif()
set(plugin_symbols
ifcopenshell_plugin_abi_v1
ifcopenshell_plugin_metadata_v1
${REGISTRATION_SYMBOL}
)
target_link_options(${TARGET} PRIVATE "SHELL:-s SIDE_MODULE=2" ${PLUGIN_OPTIMIZATION})
foreach(symbol IN LISTS plugin_symbols)
target_link_options(${TARGET} PRIVATE "LINKER:--export=${symbol}")
endforeach()
endfunction()
function(ifcopenshell_deploy_qt_runtime TARGET)
if(NOT IFCOPENSHELL_DEPLOY_QT_RUNTIME)
return()
endif()
if(NOT TARGET ${TARGET})
message(FATAL_ERROR "Cannot deploy Qt runtime for unknown target '${TARGET}'.")
endif()
get_target_property(target_type ${TARGET} TYPE)
if(NOT target_type STREQUAL "EXECUTABLE")
message(FATAL_ERROR "Qt runtime deployment target '${TARGET}' is not an executable.")
endif()
if(NOT DEFINED QT_DEFAULT_MAJOR_VERSION)
if(DEFINED QT_VERSION)
set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION})
else()
set(QT_DEFAULT_MAJOR_VERSION 6)
endif()
endif()
if(NOT TARGET Qt${QT_DEFAULT_MAJOR_VERSION}::Core)
set(qt_find_args Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS Core REQUIRED)
if(DEFINED QT_DIR AND NOT QT_DIR STREQUAL "")
list(APPEND qt_find_args PATHS ${QT_DIR})
endif()
find_package(${qt_find_args})
endif()
if(COMMAND _qt_internal_setup_deploy_support)
if(NOT DEFINED QT_CMAKE_EXPORT_NAMESPACE AND TARGET Qt${QT_DEFAULT_MAJOR_VERSION}::Core)
set(QT_CMAKE_EXPORT_NAMESPACE Qt${QT_DEFAULT_MAJOR_VERSION})
endif()
if(QT_DEFAULT_MAJOR_VERSION EQUAL 6 AND TARGET Qt6::Core)
get_target_property(qt_core_type Qt6::Core TYPE)
if(qt_core_type STREQUAL "SHARED_LIBRARY")
set(QT6_IS_SHARED_LIBS_BUILD ON)
else()
set(QT6_IS_SHARED_LIBS_BUILD OFF)
endif()
endif()
_qt_internal_setup_deploy_support()
endif()
set(deploy_args
TARGET ${TARGET}
OUTPUT_SCRIPT deploy_script
NO_UNSUPPORTED_PLATFORM_ERROR
)
if(NOT IFCOPENSHELL_DEPLOY_QT_TRANSLATIONS)
list(APPEND deploy_args NO_TRANSLATIONS)
endif()
list(APPEND deploy_args ${ARGN})
if(COMMAND qt_generate_deploy_app_script)
qt_generate_deploy_app_script(${deploy_args})
elseif(COMMAND qt6_generate_deploy_app_script)
qt6_generate_deploy_app_script(${deploy_args})
else()
message(WARNING
"Qt runtime deployment requested for '${TARGET}', but this Qt version "
"does not provide qt_generate_deploy_app_script()."
)
return()
endif()
install(SCRIPT ${deploy_script})
endfunction()
# Get a list of all OPTION flags from the CMakeLists.txt and store in an output LIST
function(get_all_option_flags output_list)
# Read the contents of the CMakeLists.txt
-4
View File
@@ -22,9 +22,6 @@ cmake -G "Ninja" ^
-D GMP_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
-D MPFR_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
-D COLLADA_SUPPORT=OFF ^
-D HDF5_SUPPORT=ON ^
-D HDF5_INCLUDE_DIR="%LIBRARY_PREFIX%\include" ^
-D HDF5_LIBRARY_DIR="%LIBRARY_PREFIX%\lib" ^
-D JSON_INCLUDE_DIR="%LIBRARY_PREFIX%\include" ^
-D PYTHON_INCLUDE_DIR=%PREFIX%\include ^
-D PYTHON_EXECUTABLE:FILEPATH=%PREFIX%\python.exe ^
@@ -37,7 +34,6 @@ cmake -G "Ninja" ^
-D GLTF_SUPPORT:BOOL=ON ^
-D BUILD_CONVERT:BOOL=ON ^
-D BUILD_IFCMAX:BOOL=OFF ^
-D IFCXML_SUPPORT:BOOL=ON ^
-D Boost_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
-D Boost_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
-D Boost_USE_STATIC_LIBS:BOOL=OFF ^
+1 -5
View File
@@ -24,9 +24,6 @@ cmake ${CMAKE_ARGS} -G Ninja \
-DMPFR_LIBRARY_DIR=$PREFIX/lib \
-DOCC_INCLUDE_DIR=$PREFIX/include/opencascade \
-DOCC_LIBRARY_DIR=$PREFIX/lib \
-DHDF5_SUPPORT:BOOL=ON \
-DHDF5_INCLUDE_DIR=$PREFIX/include \
-DHDF5_LIBRARY_DIR=$PREFIX/lib \
-DJSON_INCLUDE_DIR=$PREFIX/include \
-DCGAL_INCLUDE_DIR=$PREFIX/include \
-DLIBXML2_INCLUDE_DIR=$PREFIX/include/libxml2 \
@@ -34,7 +31,6 @@ cmake ${CMAKE_ARGS} -G Ninja \
-DEIGEN_DIR:FILEPATH=$PREFIX/include/eigen3 \
-DCOLLADA_SUPPORT:BOOL=OFF \
-DBUILD_EXAMPLES:BOOL=OFF \
-DIFCXML_SUPPORT:BOOL=ON \
-DGLTF_SUPPORT:BOOL=ON \
-DBUILD_CONVERT:BOOL=ON \
-DBUILD_IFCPYTHON:BOOL=ON \
@@ -47,4 +43,4 @@ ninja
ninja install -j 1
python "${RECIPE_DIR}/update_version_init.py" "${PKG_VERSION}" "${SP_DIR}/ifcopenshell/__init__.py"
python "${RECIPE_DIR}/update_version_init.py" "${PKG_VERSION}" "${SP_DIR}/ifcopenshell/__init__.py"
-2
View File
@@ -26,8 +26,6 @@ c_stdlib_version:
- 2.17 # [linux]
- 10.13 # [osx and x86_64]
- 11.0 # [osx and arm64]
hdf5:
- 1.14.6
libboost_devel:
- '1.86'
libxml2:
-6
View File
@@ -33,7 +33,6 @@ requirements:
- occt
- libxml2
- cgal-cpp
- hdf5
- eigen
- mpfr
- nlohmann_json
@@ -285,11 +284,6 @@ about:
<td>Internal library for IfcOpenShell</td>
<td>LGPL-3.0-or-later*</td>
</tr>
<tr>
<td>qtviewer</td>
<td>Internal library for IfcOpenShell</td>
<td>LGPL-3.0-or-later*</td>
</tr>
<tr>
<td>serializers</td>
<td>Internal library for IfcOpenShell</td>
+3
View File
@@ -0,0 +1,3 @@
.env
*.pyc
__pycache__
+3
View File
@@ -0,0 +1,3 @@
.env
*.pyc
__pycache__
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# .ifcos_env
# register autocompletes. just source the file in your shell, i.e.
# source .ifcos_env
.ifcos_env() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="create update up down restart build attach logs ps config remove help"
# Basic static completion
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
}
# Register the completion for the command "ifcos_env"
complete -F .ifcos_env ./ifcos_env
+67
View File
@@ -0,0 +1,67 @@
FROM rockylinux:9
# Update system, enable CRB (needed by some EPEL packages) and install EPEL,
# then install required packages + some common tools for a bit of command
# line comfort. Combined into one layer so a later `create` always installs
# against packages from the same dnf update, rather than layering fresh
# installs on top of a stale cached "update" layer.
RUN dnf update -y && \
dnf install -y epel-release && \
dnf config-manager --set-enabled crb && \
dnf install -y --allowerasing --setopt=install_weak_deps=False --setopt=tsflags=nodocs \
bash-completion vim git curl wget which tree htop sudo \
gcc gcc-c++ autoconf automake bison make zip cmake \
python3 python3-pip \
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
readline-devel ncurses-devel libuuid-devel git-lfs \
findutils xz byacc ccache && \
git lfs install --system && \
dnf clean all && \
rm -rf /var/cache/dnf
# Trust bind-mounted repos regardless of which user (root or builder) or host
# UID owns them, rather than a per-user config that only one of them sees.
RUN git config --system --add safe.directory '*'
# Configure ccache. CCACHE_MAXSIZE (not `ccache -M`) because /ccache is a
# volume mount point at runtime - anything `ccache -M` writes to a config
# file under it during this build gets shadowed once the real volume is
# mounted, so the size cap only actually takes effect via the env var.
# 2G is generous: a full build (IfcParse+IfcGeom+IfcConvert+wrapper, one
# Python version) measures ~300MB, and the volume is now shared across all
# checkouts (see compose.yaml), so this covers several diverging branches.
ENV CCACHE_DIR=/ccache
ENV CCACHE_MAXSIZE=2G
ENV PATH="/usr/lib/ccache:$PATH"
# Non-root user matching the host UID/GID that bind-mounts the repo (default
# 1000:1000, the common single-user-Linux-box case), so files the build
# creates under the mount keep sane, non-root ownership on the host side.
# Override with --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g)
# if your host user has a different UID/GID.
ARG USER_UID=1000
ARG USER_GID=1000
# groupadd fails outright if USER_GID is already taken by an existing
# system group - which happens whenever a host's primary GID collides with
# one baked into the rockylinux9 base image. The main real-world case is
# macOS, where the default user's primary group is "staff" at GID 20, and
# GID 20 is "games" on RHEL-family images. Only create the "builder" group
# when that GID is actually free; otherwise useradd just attaches to
# whichever group already owns it. Either way the builder user ends up
# with the right GID for bind-mount ownership, which is all that matters.
RUN (getent group "${USER_GID}" >/dev/null || groupadd -g "${USER_GID}" builder) \
&& useradd -m -u "${USER_UID}" -g "${USER_GID}" -s /bin/bash builder \
&& echo "builder ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/builder
# Copied while still root: /bin is not writable by the builder user.
COPY --from=ghcr.io/astral-sh/uv:0.11.27 /uv /uvx /bin/
USER builder
WORKDIR /__w/IfcOpenShell/IfcOpenShell
# Installed as builder so managed Python interpreters land under builder's
# $HOME, matching the user that actually runs the build.
RUN uv python install
CMD ["sleep", "infinity"]
+78
View File
@@ -0,0 +1,78 @@
Docker build environment
========================
This is a small utility to make it easy to compile a perfect `_ifcopenshell_wrapper.cpython-*-x86_64-linux-gnu.so`
files.
The reason for this tool is that I was trying to follow the web page directions, and my build was behaving differently
to the release builds. Eventually I concluded that the differences between toolchains on the RHEL based rocky9 image
and Ubuntu were just too great. Getting the build setup was already a lot of trial and error, so I thought I'd spend
more time trying to reuse the github actions that perform the build, using a utility called `act`. I learnt a lot, in
particular how much time, energy, and bandwidth Github waste. I also realised I was most of the way to a regular docker
setup anyway, so I might as well just do that. So I've deconstructed all the github action steps, and turned it into
a local docker build environment that uses the exact same base, tools, libraries, and build command/flags etc.
Right now a Github action will:
- launch the rocky9 base
- upgrade all the packages
- install a bunch of extra tools
- do a recursive checkout of your repo
- checkout the build repository
- unpack dependencies
- run the build script, making all python versions (5? right now I think)
- create the .zip release files
And it does _all_ of that _every_ time. This is not a fault of the action writers - it's just how Github seems to work.
These dockers tools do the following differently, and it's actually a bit more powerful too:
- build the base image once.
- update the packages once.
- install the extra tools once.
- the repository is the one on your host, that gets bind mounted in the container as the working directory.
- by adding an environment variable to .env, restricts to compiling for just a single python version.
- when the build is finished the created files are right there under your local repositry (but not added to git) for
ease of access
- each repository can have it's own build environment container.
- the image is shared between those environments.
- the containers share the ccache, so additional envs should get a helping hand.
- it has a simple set of user friendly commands to drive it all.
For example:
``` bash
# To see the commands (a superset of docker compose commands)
./ifcos_env
# Enable autocomplete of commands
source .ifcos_env
# First time commands
./ifcos_env create
./ifcos_env up
./ifcos_env build
# install and test library
# find an issue
# edit code
./ifcos_env build
# and so on. When done stop and optionally delete the container
./ifcos_env stop
./ifcos_env remove
```
To limit the build to one python version just add
``` bash
PY_TGT=py-311
```
or whichever version your Blender requires.
You might see UNIQUE_ID in the .env file too. This keeps containers for separate folders, separate.
System requirements
1. Linux-x64 only at this time.
2. Docker and docker-compose need to be installed.
3. Have a good amount of disk space. (image is in /var (typically the root partition) and will be about 1.7 GB)
4. The build action will create about 10GB in your repository folder. Make sure this partition is spacious
particularly if you intent on having multiple clones building.
5. ... I think that covers most of it.
+186
View File
@@ -0,0 +1,186 @@
---
name: ifcopenshell-docker-build
description: >-
Build a real ifcopenshell_wrapper (.so + .py) and IfcConvert locally via
the docker/ifcos_env toolchain, then wire them into a checkout for
running C++-dependent parts of the test suite (geometry, the SWIG
wrapper stub, the C++ parser). Use whenever a task needs to compile
IfcOpenShell's C++ core rather than just read/patch source - e.g.
reproducing or fixing a bug in src/ifcgeom, src/ifcparse, src/ifcwrap,
or validating util/scripts/validate_stub.py against the actual
generated wrapper.
---
# Building IfcOpenShell locally with docker/ifcos_env
`docker/` mirrors the project's GitHub Actions build environment locally,
in a persistent, non-root container with ccache so repeat builds are fast.
See `docker/README.md` for the design rationale. Pure-Python changes don't
need any of this - only reach for it when you need a real compiled
`_ifcopenshell_wrapper*.so` or `IfcConvert` binary.
## Placement
This `docker/` folder must live as a direct child of the repo root you want
to build (sibling of `src/`, `cmake/`, etc.) - `compose.yaml` and
`ifcos_env` resolve the repo via `../` relative to wherever `docker/`
itself sits, and bind-mount it into the container. If you're setting this
up in a fresh clone, copy the whole `docker/` directory there first.
## Setup
```bash
cd docker
./ifcos_env create # build the image (shared by name across all your clones/checkouts, so usually instant after the first time anywhere)
./ifcos_env up # create + start the container, clone/unpack the third-party dependency cache (~10GB, one-time per container)
./ifcos_env build # full build: all deps + IfcParse + IfcGeom + IfcConvert + the Python wrapper, for one Python version
```
`PY_TGT` and `UNIQUE_ID` live in `docker/.env` - `PY_TGT` (e.g. `py-311`)
restricts the build to one Python version instead of building five;
`UNIQUE_ID` is a hash of the folder path, recalculated on every `up`, so
each checkout gets its own container/volumes automatically.
A full first build takes ~1.5 hours (mostly compiling IfcOpenShell's own
C++, not the cached third-party deps). After that, ccache makes incremental
rebuilds of a couple of touched `.cpp` files **under a minute**.
## Container lifecycle
The container is long-lived (`sleep infinity`) so exec'd commands and
ccache state persist between builds. Commands map directly onto Docker
Compose's own container-vs-image distinction:
```bash
./ifcos_env up # create the container if it doesn't exist, then start it (runs ready_repo too)
./ifcos_env stop # stop the container, keep it around
./ifcos_env start # start it back up (same container, same filesystem layer)
./ifcos_env restart # stop, then start
./ifcos_env down # remove the container (and its network) entirely
./ifcos_env recreate # down, then up - a fresh container
```
Named volumes (`ccache`) and the bind-mounted repo/`build/` are unaffected
by `down`/`recreate` - only the container itself goes away, and `up`
recreates it from the image.
## Fast iteration
Pass a target to `build` to skip the parts you don't need:
```bash
./ifcos_env build IfcConvert # only the executables (IfcConvert, IfcGeomServer) - skips the Python wrapper entirely
./ifcos_env build IfcOpenShell-Python # only the SWIG Python wrapper - skips executables entirely
./ifcos_env build # no target = everything (needed the first time, or after touching shared headers)
```
Use this to keep the edit -> rebuild -> test loop fast when debugging: if
you're only touching `src/ifcgeom/`, build `IfcConvert`; if you're only
exercising the Python API, build `IfcOpenShell-Python`.
## Where the artifacts land
Build output goes to `<repo_root>/build/Linux/x86_64/install/` on the host
(bind-mounted, not just inside the container), owned by you (see
"Container user" below):
- `ifcopenshell/bin/IfcConvert` - the CLI binary
- `python-<version>/lib/python<X.Y>/site-packages/ifcopenshell/_ifcopenshell_wrapper*.so`
and `ifcopenshell_wrapper.py` - the compiled wrapper + its generated
Python glue
## Testing against a checkout (automated / AI-driven)
`_ifcopenshell_wrapper*.so` and `ifcopenshell_wrapper.py` are already
gitignored under `src/ifcopenshell-python/ifcopenshell/`, which is exactly
where a normal in-tree build would put them - copy the two files there:
```bash
SRC=build/Linux/x86_64/install/python-3.11.8/lib/python3.11/site-packages/ifcopenshell
cp "$SRC/_ifcopenshell_wrapper.cpython-311-x86_64-linux-gnu.so" src/ifcopenshell-python/ifcopenshell/
cp "$SRC/ifcopenshell_wrapper.py" src/ifcopenshell-python/ifcopenshell/
```
Then, to run the test suite against it:
```bash
export PATH="$PWD/build/Linux/x86_64/install/ifcopenshell/bin:$PATH" # for IfcConvert-dependent tests
cd src/ifcopenshell-python/test
PYTHONPATH="$PWD/.." python3.11 -m pytest -p no:pytest-blender .
```
(`-p no:pytest-blender` avoids the pytest-blender plugin trying to find a
`blender` executable and failing collection entirely, even for non-Blender
tests.) You'll need the matching Python version's `pip install`s too
(numpy, shapely, isodate, lark, tabulate, pytest, ... - whatever the
modules under test import) since this is a bare interpreter, not the
project's pixi env.
**This is the pattern to use for automated or AI-driven verification.**
Don't use `try` (below) for that - it overwrites files in a real, live
Blender installation, which isn't something an automated/AI workflow
should ever do without the human explicitly asking for it in the moment.
## Testing in Blender itself (human only)
`try` copies the built wrapper straight into your actual Blender/Bonsai
extension install, for manual in-Blender testing:
```bash
./ifcos_env try
```
It reads `BLENDER_USER_RESOURCE` from `.env` - set this to wherever
Blender's user resource folder for the Bonsai extension actually lives on
your system, which depends on your own Blender setup:
```bash
# in docker/.env
BLENDER_USER_RESOURCE=~/.config/blender/bonsai/
```
`try` figures out the built Python version from `build/.../install/`
(disambiguating with `PY_TGT` if more than one version was built) and
copies the wrapper to
`$BLENDER_USER_RESOURCE/extensions/.local/lib/python<X.Y>/site-packages/ifcopenshell/`.
## Container user
The image runs as a non-root `builder` user, UID/GID matching your host
account (passed as `--build-arg` by `create` from `id -u`/`id -g`, so it
adjusts automatically - no manual flag needed even if you're not 1000:1000).
Files the build creates under the bind mount come out owned by you, not
root. Passwordless `sudo` is available inside the container (e.g. via
`attach`) for the rare case you need root for something ad hoc.
If you're picking up an existing checkout that was previously built with
an older, root-based image, you may hit `Permission denied` the first time
you run `up`/`build` under the new image - `build/`, `.git/modules/`, the
`ccache` volume, `output/`, and `build.log` can all be left root-owned from
before. Fix it once via the container's own root (no host `sudo` needed):
```bash
docker exec -u root -w /__w/IfcOpenShell/IfcOpenShell <container-name> \
chown -R "$(id -u)":"$(id -g)" .git/modules build output build.log /ccache
```
(`<container-name>` is `ifcopenshell-<UNIQUE_ID>` - see `docker ps -a`.)
## Other things worth knowing
- **Linux x64 only.** `compose.yaml` pins `platform: linux/amd64`; on an
ARM host (e.g. Apple Silicon) this build isn't available.
- **The final "Package .zip archives" step of `build()` has a pre-existing
bash syntax error**, unrelated to compilation - the actual build already
succeeded by that point (look for `Built IfcOpenShell...` in the output),
so this is safe to ignore if you only need the raw artifacts under
`build/.../install/`, not packaged release zips.
- **`test_mmaped_stream` and similar `USE_MMAP`-dependent tests will fail**
against this build - `nix/build-all.py` is invoked with `USE_MMAP=OFF`
here. Not a bug in your code if you see it fail.
- Only the bind-mounted `<repo>/build` lives on the host filesystem your
repo is checked out on. Anything the container writes *outside* that
mount lives in the container's own writable layer under Docker's data
root (commonly `/var/lib/docker`, i.e. usually your root partition) -
keep an eye on `df -h /` if you're running several of these containers
at once.
+15
View File
@@ -0,0 +1,15 @@
name: ifcopenshell-${UNIQUE_ID}
services:
ifcopenshell:
container_name: ifcopenshell-${UNIQUE_ID}
image: ifcopenshell-build-env:updated
platform: linux/amd64
volumes:
- type: bind
source: ../
target: /__w/IfcOpenShell/IfcOpenShell
- ccache:/ccache
volumes:
ccache:
name: ifcopenshell-ccache-shared
+339
View File
@@ -0,0 +1,339 @@
#!/bin/bash
# ================== CONFIG ==================
SCRIPT_NAME=$(basename "$0")
ENV_FILE=".env"
WORKDIR="/__w/IfcOpenShell/IfcOpenShell"
NAMEPREFIX=ifcopenshell
function set_env() {
# Load .env file if it exists
if [[ -f "$ENV_FILE" ]]; then
set -a
source "$ENV_FILE"
set +a
echo "✅ Loaded environment variables from $ENV_FILE"
else
echo "⚠️ No $ENV_FILE found, proceeding without it."
fi
}
set_env
# ================ FUNCTIONS =================
function create() {
echo "⭐ Creating image: ifcopenshell-build-env"
docker build -f Dockerfile \
--build-arg USER_UID="$(id -u)" --build-arg USER_GID="$(id -g)" \
-t ifcopenshell-build-env:updated .
}
function update() {
# The Dockerfile always builds FROM a clean rockylinux:9 and does
# `dnf update -y` as its first step, so re-running create() is enough
# to get fresh packages.
echo "⚡ Updating image: ifcopenshell-build-env"
create
}
function up() {
# Creates the container if it doesn't exist yet (and starts it either
# way) - this is the one that needs ready_repo, since a freshly created
# container has no submodules/dependency cache in place yet.
echo "🚀 Creating/starting stack: ifcopenshell-${UNIQUE_ID}"
unique # Update UNIQUE_ID first
docker compose up -d "$@" # Container must exist before ready_repo can exec into it.
ready_repo # Ensure repo is recursive, and the build repo is in place.
}
function down() {
# Removes the container (and its network) entirely. Named volumes
# (ccache) and the bind-mounted repo/build/ survive; up() will recreate
# the container from scratch next time.
echo "🔥 Removing stack: ifcopenshell-${UNIQUE_ID}"
docker compose down "$@"
}
function stop() {
# Stops the existing container without removing it - the container,
# its filesystem layer, and its exec history all remain intact.
echo "🛑 Stopping stack: ifcopenshell-${UNIQUE_ID}"
docker compose stop "$@"
}
function start() {
# Starts a previously-stopped container back up. Does nothing (and
# won't create anything) if the container doesn't exist - use up() for
# that.
echo "▶️ Starting stack: ifcopenshell-${UNIQUE_ID}"
docker compose start "$@"
}
function restart() {
echo "🔄 Restarting stack (stop, then start)..."
stop
start
}
function recreate() {
echo "♻️ Recreating stack (down, then up)..."
down
up
}
function logs() {
echo "📜 Showing logs..."
docker compose logs -f "$@"
}
function ps() {
docker compose ps
}
function config() {
echo "🔍 Validated compose configuration:"
docker compose config
}
function remove() {
# Lower-level than down(): removes already-stopped containers without
# touching the compose network. Mostly useful after a plain stop().
echo "🗑️ Removing stopped containers: ifcopenshell-${UNIQUE_ID}"
docker compose rm "$@"
}
function unique() {
echo "🔧 Making stack name folder specific..."
REGEX="^UNIQUE_ID="
if [[ ! -f "$ENV_FILE" ]] || ! grep -qE "$REGEX" "$ENV_FILE"; then
echo -e "\nUNIQUE_ID=dummy\n" >> "$ENV_FILE"
fi
export UNIQUE_ID="$(pwd | sha256sum | cut -c -8)"
# `sed -i` takes incompatible syntax between GNU sed (Linux) and BSD sed
# (macOS) - `-si` is GNU-only and errors as "illegal option -- s" under
# BSD/macOS sed. Avoid -i altogether and do the in-place edit via a temp
# file + mv instead, which behaves identically with either sed.
local tmp_file
tmp_file="$(mktemp "${ENV_FILE}.XXXXXX")"
sed "s/^UNIQUE_ID=.*$/UNIQUE_ID=${UNIQUE_ID}/" "$ENV_FILE" > "$tmp_file"
mv "$tmp_file" "$ENV_FILE"
set_env
}
function ready_repo() {
echo "👍 Getting the repo ready to build..."
docker exec -i -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
set -euo pipefail # Recommended for robustness
git submodule update --init --recursive
if [[ ! -d "build" ]]; then
git clone -b rockylinux9-x64 https://github.com/IfcOpenShell/build-outputs.git build
else
cd build
git pull
cd ..
fi
if [[ ! -d "build/Linux/x86_64/install/boost-1.86.0/" ]]; then
cd build
uv run ../nix/cache_dependencies.py unpack
cd ..
fi
'
}
function build() {
echo "☕ Execute the build, go make yourself a cuppa... I'll be a while"
local BUILD_TARGET="$1"
docker exec -i -w "${WORKDIR}" -e PY_TGT="${PY_TGT}" -e BUILD_TARGET="${BUILD_TARGET}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
set -o pipefail
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release uv run ./nix/build-all.py -v ${PY_TGT:+-$PY_TGT} --diskcleanup ${BUILD_TARGET} 2>&1 | tee build.log
'
echo "🎒 Pack Dependencies"
docker exec -i -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
cd build
uv run ../nix/cache_dependencies.py pack
'
echo "🎁 Package .zip archives"
docker exec -i -w "${WORKDIR}" -e GITHUB_SHA="$(git rev-parse HEAD)" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
OUTPUT_DIR=${PWD}/output
VERSION=v`cat VERSION`
mkdir -p ${OUTPUT_DIR}
cd ./build/`uname`/*/install/ifcopenshell
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
numbers=`echo $py_version | grep -oE "[0-9]+\.[0-9]+" | tr -d "."`
py_version_major=python-${numbers}$postfix
pushd . > /dev/null
cd $py_version
if [ ! -d ifcopenshell ]; then
mkdir ../ifcopenshell_
mv * ../ifcopenshell_
mv ../ifcopenshell_ ifcopenshell
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell/*
mv *.zip ${OUTPUT_DIR}/
popd > /dev/null
done
cd bin
if compgen -G "./*.zip" > /dev/null; then
rm *.zip 2>&1 >/dev/null || true
ls | while read exe; do
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip $exe
done
mv *.zip ${OUTPUT_DIR}/
cd ..
'
}
function attach() {
echo "🔦 Connect to interactive shell"
docker exec -it -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" /bin/bash
}
function try() {
# Copies the freshly built wrapper into your actual Blender/Bonsai
# installation for manual, in-Blender testing. This is a human-only
# convenience: it overwrites files in your live Blender setup, so it's
# not something that should run unattended as part of an automated or
# AI-driven build/test loop (which should instead copy the wrapper into
# the repo's own src/ifcopenshell-python/ifcopenshell/ - see SKILL.md).
echo "🚴 Copying build artifacts into your Blender resource folder for testing"
if [[ -z "${BLENDER_USER_RESOURCE:-}" ]]; then
echo "❌ BLENDER_USER_RESOURCE is not set in .env."
echo " Add a line pointing at wherever Blender's user resource folder for"
echo " the Bonsai extension actually is on your system, e.g.:"
echo " BLENDER_USER_RESOURCE=~/.config/blender/bonsai/"
return 1
fi
# Normalise: expand a leading ~ (in case it was quoted in .env and so
# never went through shell tilde-expansion when set_env sourced it),
# then resolve to an absolute, symlink-free path.
local resource="${BLENDER_USER_RESOURCE/#\~/$HOME}"
resource="$(realpath -m "$resource")"
local install_dir="../build/Linux/x86_64/install"
local py_dirs=("$install_dir"/python-*)
if [[ ${#py_dirs[@]} -gt 1 && -n "${PY_TGT:-}" ]]; then
# PY_TGT is compact (py-311); the install dirs are dotted
# (python-3.11.8) - reinsert the dot (assumes a single-digit major
# version, true for the Python 3.x line) before matching.
local py_tgt_digits="${PY_TGT#py-}"
local py_tgt_dotted="${py_tgt_digits:0:1}.${py_tgt_digits:1}"
local filtered=() d
for d in "${py_dirs[@]}"; do
[[ "$(basename "$d")" == "python-${py_tgt_dotted}."* ]] && filtered+=("$d")
done
[[ ${#filtered[@]} -gt 0 ]] && py_dirs=("${filtered[@]}")
fi
if [[ ${#py_dirs[@]} -ne 1 || ! -d "${py_dirs[0]}" ]]; then
echo "❌ Expected exactly one built python-* dir under $install_dir, found ${#py_dirs[@]}."
echo " Run 'build' first, or set PY_TGT in .env to disambiguate a multi-version build."
return 1
fi
local py_minor
py_minor="$(basename "${py_dirs[0]}" | grep -oE '[0-9]+\.[0-9]+')"
local wrapper_dir="${py_dirs[0]}/lib/python${py_minor}/site-packages/ifcopenshell"
if [[ ! -f "$wrapper_dir/ifcopenshell_wrapper.py" ]]; then
echo "❌ Built wrapper not found at $wrapper_dir - run 'build' first."
return 1
fi
local target="$resource/extensions/.local/lib/python${py_minor}/site-packages/ifcopenshell"
mkdir -p "$target"
cp "$wrapper_dir"/_ifcopenshell_wrapper*.so "$target/"
cp "$wrapper_dir"/ifcopenshell_wrapper.py "$target/"
echo "✅ Copied wrapper into $target"
}
function clean() {
# Host-side only - doesn't touch the container, image, or ccache volume.
echo "💎 Clean the build and output folder up"
if [[ -d "../build" ]]; then
rm -rf ../build
fi
if [[ -d "../output" ]]; then
rm -rf ../output
fi
}
function help() {
cat <<EOF
Usage: ./$SCRIPT_NAME <command>
Available commands:
create Build the rocky9-based image
update Rebuild the image fresh, picking up OS package updates
up Create the container if it doesn't exist yet, and start it
down Remove the container entirely (docker compose down)
stop Stop the container without removing it
start Start a previously-stopped container
restart stop, then start (same container, no recreation)
recreate down, then up (fresh container)
build Execute the IfcOpenShell build
attach Connect to an interactive shell in the container
try Copy the built wrapper into your Blender resource folder
(human-only - see BLENDER_USER_RESOURCE below, and SKILL.md
for the AI/automated-testing equivalent)
clean Remove the build and output folders
logs Follow container logs
ps Show running containers
config Validate and show compose config
remove Remove stopped containers (docker compose rm)
help Show this help
Environment variables from .env are automatically loaded, including:
PY_TGT Restrict the build to one Python version, e.g. py-311
UNIQUE_ID Recalculated automatically on every 'up', don't set by hand
BLENDER_USER_RESOURCE Where 'try' copies the wrapper for manual testing, e.g.
~/.config/blender/bonsai/
EOF
}
# ================= MAIN =================
case "$1" in
create) create ;;
update) update ;;
up) up "${@:2}" ;;
down) down "${@:2}" ;;
stop) stop "${@:2}" ;;
start) start "${@:2}" ;;
restart) restart ;;
recreate) recreate ;;
build) build "${@:2}" ;;
attach) attach ;;
try) try ;;
clean) clean ;;
logs) logs "${@:2}" ;;
ps) ps ;;
config) config ;;
remove) remove ;;
help|-h|--help) help ;;
"")
echo "❌ No command provided."
help
;;
*)
echo "❌ Unknown command: $1"
echo "Type './$SCRIPT_NAME help' for available commands."
exit 1
;;
esac
+317
View File
@@ -0,0 +1,317 @@
# Build fix: remove `boost_system` from CMake components
`Boost.System` became header-only in Boost 1.69. Boost 1.90.0 no longer ships a compiled library or CMake config for it, so `find_package(Boost REQUIRED COMPONENTS system ...)` fails.
## Fix
`cmake/CMakeLists.txt`:
```diff
- set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
+ set(BOOST_COMPONENTS program_options regex thread date_time iostreams)
```
The headers are still available; no linking is needed.
# Build fix: add `template` keyword for dependent template member calls
Calling a template member function through a dependent expression (e.g. `storage->has_attribute_value<T>(...)` where `storage`'s type depends on a template parameter) requires the `template` keyword to disambiguate from a less-than comparison.
## Error
```
src/ifcparse/IfcParse.cpp:1856:67: error: expected primary-expression before '>' token
1856 | if (storage->has_attribute_value<express::Base>(attr_index)) {
| ^
```
Six identical errors at lines 1856, 1865, 1896, 1905, 1934, 1943.
## Fix
`src/ifcparse/IfcParse.cpp`:
```diff
-storage->has_attribute_value<express::Base>(attr_index)
+storage->template has_attribute_value<express::Base>(attr_index)
-storage->has_attribute_value<Blank>(attr_index)
+storage->template has_attribute_value<Blank>(attr_index)
```
Applied at all six call sites in `in_memory_file_storage::read_from_stream`.
# Linker fix: missing explicit template instantiations for `InstanceStreamer`
`InstanceStreamer` is a class template with methods defined in `IfcParse.cpp`, not the header. Without explicit instantiations, the linker can't find the symbols when the SWIG wrapper loads.
## Error
```
ImportError: undefined symbol: _ZN8IfcParse16InstanceStreamerINS_10FileReaderINS_14FullBufferImplEEEEC1EPS3_PNS_7IfcFileE
(IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(FileReader<FullBufferImpl>*, IfcFile*))
```
## Fix
Cannot use `template class InstanceStreamer<...>` because some constructors have `static_assert` guards that reject certain reader types. Instead, instantiate each member function individually per reader type, only including the constructors valid for that type.
`src/ifcparse/IfcParse.cpp` (after the last `InstanceStreamer` method definition):
```cpp
// FullBufferImpl
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(IfcParse::IfcFile*);
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(const std::string&, bool, IfcParse::IfcFile*);
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(void*, int, IfcParse::IfcFile*);
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(FileReader<FullBufferImpl>*, IfcParse::IfcFile*);
// ... plus ensure_header, initialize_header, hasSemicolon, semicolonCount,
// pushPage, bypassTypes, readInstance
// PushedSequentialImpl — same pattern, different valid constructors
// MMapFileReader (ifdef USE_MMAP) — same pattern
```
# Linker fix: `FullBufferImpl` missing buffer constructor
SWIG's `stream_from_string` calls `InstanceStreamer<FileReader<FullBufferImpl>>(void*, int, IfcFile*)`, but the `(void*, int)` constructor previously hit a `static_assert` for `FullBufferImpl` — it only allowed `PushedSequentialImpl`.
## Error
```
ImportError: undefined symbol: _ZN8IfcParse16InstanceStreamerINS_10FileReaderINS_14FullBufferImplEEEEC1EPviPNS_7IfcFileE
(InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(void*, int, IfcFile*))
```
## Fix
Three changes to make `FullBufferImpl` support buffer-based and default construction:
`src/ifcparse/FileReader.h` — add buffer constructor to `FullBufferImpl`:
```diff
class IFC_PARSE_API FullBufferImpl {
public:
explicit FullBufferImpl(const std::string& fn);
+ FullBufferImpl(void* data, size_t length);
```
`src/ifcparse/FileReader.h` — add `FileReader(void*, size_t)` forwarding constructor:
```diff
+ FileReader(void* data, size_t length)
+ : cursor_(0) {
+ if constexpr (std::is_same_v<Impl, FullBufferImpl>) {
+ impl_ = std::make_shared<Impl>(data, length);
+ } else {
+ static_assert(...);
+ }
+ }
```
`src/ifcparse/FileReader.cpp` — implement the constructor:
```cpp
FullBufferImpl::FullBufferImpl(void* data, size_t length)
: buf_(static_cast<char*>(data), static_cast<char*>(data) + length)
, size_(length) {
}
```
`src/ifcparse/IfcParse.cpp` — extend the two `InstanceStreamer` constructors to accept `FullBufferImpl`:
```diff
// InstanceStreamer(IfcFile*):
+ } else if constexpr (std::is_same_v<Reader, FileReader<FullBufferImpl>>) {
+ owned_stream_ = std::make_unique<Reader>(nullptr, (size_t)0);
// InstanceStreamer(void*, int, IfcFile*):
+ } else if constexpr (std::is_same_v<Reader, FileReader<FullBufferImpl>>) {
+ owned_stream_ = std::make_unique<Reader>(data, (size_t)length);
```
# Runtime fix: segfault in `parse_context::push()` due to vector reallocation
`parse_context_pool` stores nodes in a `std::vector<parse_context>`. During parsing, `load()` takes a `parse_context&` parameter and calls `context.push()`, which calls `pool_->make()`. If the pool's vector reallocates (via `emplace_back`), all existing references into the vector — including the `context` reference held by the caller — become dangling. Subsequent access through the dangling reference causes a segfault.
Triggered by larger IFC files (e.g. `ISSUE_159_kleine_Wohnung_R22.ifc`, 9.5 MB) that cause enough pool growth to trigger reallocation.
## Error
```
Thread 1 received signal SIGSEGV, Segmentation fault.
0x... in IfcParse::parse_context::push()
#1 in_memory_file_storage::load(...) // context& is dangling after reallocation
#2 in_memory_file_storage::load(...) // parent call
#3 InstanceStreamer::readInstance()
```
## Fix
`src/ifcparse/storage.h` — change the pool container from `std::vector` to `std::deque`, which does not invalidate references on `push_back`/`emplace_back`:
```diff
+#include <deque>
struct parse_context_pool {
- std::vector<parse_context> nodes_;
+ std::deque<parse_context> nodes_;
```
# Runtime fix: `express::Base` comparison operators throw on null/expired instances
`express::Base::operator<` and `operator==` called `data()`, which throws `std::runtime_error("Trying to access deleted instance reference")` when the internal `weak_ptr` is expired. A default-constructed `express::Base` (the value-type equivalent of a null pointer) always has an expired `weak_ptr`.
## Why this model triggers it
The bug requires two conditions to coincide:
1. A representation is shared by **more than one product** (via `IfcRepresentationMap` / `IfcMappedItem`).
2. At least one of those products has **no material association**, so `get_single_material_association()` returns `express::Base{}` (the null equivalent).
In `advanced_model.ifc`, Body representations like `#449` (Body/Brep) have a single `IfcRepresentationMap` (`#453`) with 13 `IfcMappedItem` usages, meaning 13 products share the geometry. Some of those products (e.g. `IfcFlowTerminal` instances) have no `IfcRelAssociatesMaterial`, so `get_single_material_association` returns `express::Base{}`.
Smaller or simpler models don't hit this because either:
- Every representation maps to only 1 product → `reuse_ok_` short-circuits at `products.size() == 1` before reaching the material check.
- Every product has a material association → no null `express::Base` is ever inserted into the set.
## Exact call sequence
```
Iterator::initialize()
try {
mapping::get_representations(reps, filters_)
addRepresentationsFromDefaultContexts(representations)
→ collects reps from subcontexts in order:
Axis (#115): 143 reps
Body (#117): 7550 reps
FootPrint (#119): 12 reps
for (auto representation : representations):
── Axis reps (indices 0142) ──────────────────────────
products_represented_by(rep, rmap)
→ OfProductRepresentation: 1 product each
filter_products(products, filters) → 1 product
reuse_ok_(ifcproducts)
→ products.size() == 1 → return true ← SHORT-CIRCUIT, no material check
representation_mapped_to(rep) → null (no MappedItem)
→ task created. 143 tasks accumulated.
── First Body rep #449 (Body/Brep) ────────────────────
products_represented_by(#449, rmap)
→ OfProductRepresentation: empty
→ RepresentationMap: 1 map (#453)
→ MapUsage: 13 MappedItems → traces through to 13 IfcProducts
filter_products(products, filters) → 13 products
reuse_ok_(ifcproducts) ← CRASH HERE
→ products.size() == 1? NO (13 products)
→ for each product:
find_openings(product) → OK
get_single_material_association(product)
→ some products have no IfcRelAssociatesMaterial
→ returns express::Base{} (expired weak_ptr)
associated_single_materials.insert(result)
→ std::set::insert calls operator<
→ operator< calls data()
→ data() calls data_.lock() → expired → THROWS
"Trying to access deleted instance reference"
} catch (const std::exception& e) {
Logger::Error(e) ← exception caught here, get_representations aborted
}
→ reps contains only the 143 Axis tasks created before the throw
→ all 143 Axis reps have Curve2D geometry → map(representation) returns null
→ no valid elements produced → initialize() returns false
```
In the old pointer-based code, `reuse_ok_` used `std::set<const IfcUtil::IfcBaseEntity*>` and `get_single_material_association` returned `nullptr`. Inserting `nullptr` into a `std::set<T*>` is a plain pointer comparison — no dereference, no throw. The refactoring to `std::set<express::Base>` changed the comparison from pointer comparison to `express::Base::operator<`, which unconditionally dereferences through `data()`.
## Error
```
[Error] Trying to access deleted instance reference
[Notice] Created 143 tasks for 143 products ← only Axis reps; all Body reps lost
initialize() returned: False
```
## Fix
`src/ifcparse/express.h` — use `weak_ptr::lock().get()` instead of `data()` so that expired pointers compare as `nullptr` (matching old raw-pointer semantics):
```diff
bool operator<(const Base& other) const {
- return data() < other.data();
+ auto a = data_.lock();
+ auto b = other.data_.lock();
+ return a.get() < b.get();
}
bool operator==(const Base& other) const {
- return data() == other.data();
+ auto a = data_.lock();
+ auto b = other.data_.lock();
+ return a.get() == b.get();
}
```
# Runtime fix: `entity_instance` missing `get_inverse` due to SWIG `%rename` collision
Accessing inverse attributes (e.g. `element.IsDecomposedBy`) on any entity raises `AttributeError: entity instance of type 'IFC2X3.IfcProject' has no attribute 'get_inverse'`.
## Why
`entity_instance_mixin.__getattr__` (line 106 of `entity_instance.py`) calls `self.get_inverse(name)` when it detects an inverse attribute. Since the mixin inherits into the SWIG-generated `entity_instance` class (via the `object = custom_base` hack in `IfcParseWrapper.i:936`), `self.get_inverse` must resolve to a method on the SWIG class.
However, `IfcParseWrapper.i:70` has a global rename:
```
%rename("get_inverses_by_declaration") get_inverse;
```
This was intended for `ifcopenshell::file::get_inverse` (which takes an entity + declaration and returns instances by reference), but SWIG `%rename` is global — it also renames the `%extend express::Base` method `get_inverse(const std::string& a)` at line 551. So the Python-side `entity_instance` class exposes the method as `get_inverses_by_declaration`, not `get_inverse`.
The old code (`v0.8.0`) didn't hit this because `__getattr__` called `self.wrapped_data.get_inverse(name)` on an inner `ifcopenshell_wrapper.entity_instance` object — but in that old layout, the inner object was constructed differently and the rename didn't apply the same way (or the method had a different path). In the new mixin approach, `self` **is** the SWIG object, so the rename is directly visible.
## Fix
`src/ifcwrap/IfcParseWrapper.i` — override the global rename specifically for `express::Base::get_inverse`, restoring the original name on entity instances:
```diff
+%rename("get_inverse") express::Base::get_inverse;
%rename("get_inverses_by_declaration") get_inverse;
```
Add this line **before** the global rename (or anywhere before the `%extend express::Base` block). This scoped rename takes precedence for `express::Base`, so:
- `entity_instance.get_inverse(name)` works as the mixin expects
- `file.get_inverses_by_declaration(...)` keeps its intended name
## Python-side workaround
`entity_instance.py:106` — call the method by its SWIG-renamed name:
```diff
- vs = self.get_inverse(name)
+ vs = self.get_inverses_by_declaration(name)
```
# Runtime fix: `entity_instance` class no longer importable from `entity_instance` module
The class rename from `entity_instance` to `entity_instance_mixin` broke external code that does `from ifcopenshell.entity_instance import entity_instance`.
## Error
```
ImportError: cannot import name 'entity_instance' from 'ifcopenshell.entity_instance'
```
Triggered at import time via `ifcopenshell.util.pset` (and likely other modules).
## Fix
`src/ifcopenshell-python/ifcopenshell/entity_instance.py` — add a backwards-compatible alias at the bottom of the module:
```python
entity_instance = entity_instance_mixin
```
+305 -204
View File
@@ -1,4 +1,6 @@
#!/usr/bin/python
# /// script
# ///
###############################################################################
# #
# This file is part of IfcOpenShell. #
@@ -20,7 +22,7 @@
"""
Example usage:
# Build all targets by default.
# Build all targets by default, except BonsaiViewer (it's set explicitly).
python build-all.py
# Build just the provided targets.
@@ -48,7 +50,7 @@ Used environment variables:
- ``NO_CLEAN`` - do not clean `ifcopenshell` build directories but continue working on current build
(installed dependencies are never cleared).
By default option is disabled, to enable pass any value from `1`, `on`, `true`.
- ``IFCOS_SCHEMAS`` - schemas to be built; defaults to cmake default (IFC2X3; IFC4; IFC4X3_ADD2) - to be supplied as `2x3;4`
- ``IFCOS_SCHEMAS`` - schemas to be built; defaults to cmake default (8 schemas), to be supplied as `2x3;4;4x3_add2`
- ``USE_OCCT`` - whether to use official Open CASCADE instead of Community Edition
(`true` by default, any other value is considered `false`)
- ``WASM_PYTHON_PATH`` - path to WASM Python installation,
@@ -58,6 +60,7 @@ Used environment variables:
Example value: 'pyodide/cpython/installs/python-3.13.2'
- ``ADD_COMMIT_SHA`` - if defined with any non-empty value then
`ADD_COMMIT_SHA` and `VERSION_OVERRIDE` will be set to `ON` while configuring IfcOpenShell
- ``BUILD_BONSAIVIEWER`` - enable building BonsaiViewer, value of the env variable has to be truthy.
# This script builds IfcOpenShell and its dependencies #
# #
@@ -75,27 +78,30 @@ Used environment variables:
# #
# for python37 to install correctly additionally: #
# * libffi(-dev[el]) #
# for Python build we also needs ssl #
# for Python build we also needs ssl and zlib #
# (since we do `pip install numpy` at the end) #
# * libssl-dev #
# #
# on debian 7.8 these can be obtained with: #
# $ apt-get install git gcc g++ autoconf bison bzip2 cmake #
# mesa-common-dev libffi-dev libfontconfig1-dev #
# libssl-dev xz #
# libssl-dev xz zlib1g-dev #
# #
# on ubuntu 14.04: #
# $ apt-get install git gcc g++ autoconf bison make cmake #
# mesa-common-dev libffi-dev libfontconfig1-dev #
# libssl-dev xz-utils #
# libssl-dev xz-utils zlib1g-dev #
# #
# on OS X El Capitan with homebrew: #
# $ brew install git bison autoconf automake libffi cmake #
# $ # `bison` shipped with Mac is too old for swig build, #
# $ # so we use `brew`. #
# $ export PATH=$(brew --prefix bison)/bin:$PATH #
# #
# on RHEL-related distros: #
# $ yum install git gcc gcc-c++ autoconf bison make cmake #
# $ dnf install git gcc gcc-c++ autoconf bison make cmake #
# mesa-libGL-devel libffi-devel fontconfig-devel bzip2 #
# automake patch byacc xz #
# automake patch byacc xz zlib-devel openssl-devel #
"""
@@ -104,7 +110,6 @@ import logging
import multiprocessing
import os
import platform
import re
import shutil
# @todo temporary for expired mpfr.org certificate on 2023-04-08
@@ -116,21 +121,14 @@ import tarfile
import threading
from datetime import datetime
ssl._create_default_https_context = ssl._create_unverified_context
ssl._create_default_https_context = ssl._create_unverified_context # ty:ignore[invalid-assignment]
import time
from collections.abc import Generator, Sequence
from pathlib import Path
from typing import Literal, Union
from urllib.request import urlretrieve
try:
from typing import Literal, Union
except:
# python 3.6 compatibility for rocky 8
from typing import Union
from typing_extensions import Literal
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
ch = logging.StreamHandler()
@@ -139,6 +137,7 @@ logger.addHandler(ch)
PROJECT_NAME = "IfcOpenShell"
USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION")
ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
BUILD_BONSAIVIEWER = os.getenv("BUILD_BONSAIVIEWER", "").lower() in {"1", "on", "true", "yes"}
PYTHON_VERSIONS = ["3.10.3", "3.11.8", "3.12.1", "3.13.6", "3.14.0"]
JSON_VERSION = "3.11.3"
@@ -147,19 +146,20 @@ OCCT_VERSION = "7.8.1"
BOOST_VERSION = "1.86.0"
EIGEN_VERSION = "3.4.0"
PCRE_VERSION = "8.41"
PCRE2_VERSION = "10.32"
LIBXML2_VERSION = "2.13.8"
SWIG_VERSION = "4.1.0"
SWIG_VERSION = "4.2.1"
OPENCOLLADA_VERSION = "v1.6.68"
HDF5_VERSION = "1.13.1"
GMP_VERSION = "6.3.0"
MPFR_VERSION = "3.1.6" # latest is 4.1.0
CGAL_VERSION = "v5.6.3"
USD_VERSION = "23.05"
TBB_VERSION = "2021.9.0"
ROCKSDB_VERSION = "9.11.2"
ROCKSDB_VERSION = "10.4.2"
ZSTD_VERSION = "1.5.7"
MANIFOLD_VERSION = "3.2.1"
QT6_VERSION = os.getenv("QT6_VERSION", "6.8.3")
# binaries
cp = "cp"
bash = "bash"
@@ -246,15 +246,8 @@ if WASM:
# https://github.com/pyodide/pyodide-build/pull/249
WASM_CMAKE_IS_USING_INIT_VARS = get_pyodide_build_version() >= (99, 0, 0)
# pyodide provide empty `CXXFLAGS`, leading to issues using C++ files compiled with `-fexceptions`
# which is used by OCCT.
# https://github.com/pyodide/pyodide-build/issues/251
side_module_cxx_flags = os.environ.get("SIDE_MODULE_CXXFLAGS", "")
if side_module_cxx_flags.strip():
print("SIDE_MODULE_CXXFLAGS are already passed from pyodide build ('{side_module_cxx_flags}').")
print("Maybe it's time to stop overriding them in the script?")
os.environ["SIDE_MODULE_CXXFLAGS"] = os.environ["SIDE_MODULE_CFLAGS"]
# 0.31 is required for SIDE_MODULE_CXXFLAGS to be provided.
assert get_pyodide_build_version() >= (0, 31)
# Set defaults for missing empty environment variables
@@ -294,6 +287,7 @@ DEPS_DIR = os.getenv("DEPS_DIR", DEFAULT_DEPS_DIR)
if not os.path.exists(DEPS_DIR):
os.makedirs(DEPS_DIR)
INSTALL_DIR = Path(DEPS_DIR) / "install"
BUILD_CFG = os.getenv("BUILD_CFG", "RelWithDebInfo")
@@ -319,45 +313,40 @@ cecho(f"* Build Directory = {BUILD_DIR}", MAGENTA)
cecho(f"* Dependency Directory = {DEPS_DIR}", MAGENTA)
cecho(f" - The directory where {PROJECT_NAME} dependencies are installed.")
cecho(f"* Build Config Type = {BUILD_CFG}", MAGENTA)
cecho(
""" - The used build configuration type for the dependencies.
Defaults to RelWithDebInfo if not specified."""
)
cecho(""" - The used build configuration type for the dependencies.
Defaults to RelWithDebInfo if not specified.""")
if BUILD_CFG == "MinSizeRel":
cecho(" WARNING: MinSizeRel build can suffer from a significant performance loss.", RED)
cecho(f"* IFCOS_NUM_BUILD_PROCS = {IFCOS_NUM_BUILD_PROCS}", MAGENTA)
cecho(
""" - How many compiler processes may be run in parallel.
"""
)
cecho(""" - How many compiler processes may be run in parallel.
""")
cecho(f" * IFCOS_SCHEMAS = '{os.environ.get('IFCOS_SCHEMAS')}'", MAGENTA)
cecho(
""" - IFC Schemas to compile. If not provided, fallback to default provided in cmake.
"""
)
cecho(""" - IFC Schemas to compile. If not provided, fallback to default provided in cmake.
""")
dependency_tree: "dict[str, tuple[str, ...]]" = {
"IfcParse": ("boost", "libxml2", "hdf5", "rocksdb"),
"IfcGeom": ("IfcParse", "occ", "json", "cgal", "eigen", "OpenCOLLADA"),
"IfcParse": ("boost", "libxml2", "rocksdb"),
"IfcGeom": ("IfcParse", "occ", "manifold", "json", "cgal", "eigen", "OpenCOLLADA"),
"IfcConvert": ("IfcGeom",),
"OpenCOLLADA": ("libxml2", "pcre"),
"IfcGeomServer": ("IfcGeom",),
"IfcOpenShell-Python": ("python", "swig", "IfcGeom"),
"swig": ("pcre2",),
"BonsaiViewer": ("IfcGeom", "qt6"),
"swig": (),
"boost": (),
"libxml2": (),
"python": (),
"occ": (),
"pcre": (),
"pcre2": (),
"json": (),
"hdf5": (),
"cgal": (),
"eigen": (),
"rocksdb": ("zstd",),
"zstd": (),
"manifold": (),
"qt6": (),
# 'usd': ('boost', 'oneTBB')
}
@@ -411,17 +400,29 @@ else:
targets = set(dependency_tree.keys())
targets = set(t for t in targets if "without-%s" % t.lower() not in flags)
if not explicit_targets and not BUILD_BONSAIVIEWER:
targets.difference_update({"BonsaiViewer", "qt6"})
if BUILD_BONSAIVIEWER:
targets.update(gather_dependencies("BonsaiViewer"))
# Opt-out for the Python wrapper. Currently used by the bonsai CI workflow
# on macOS, where the post-plug-in-refactor wrapper hard-links
# ifcopenshell.document.rdb.dylib but the CREATE_BUNDLE install rule does
# not actually drop the dylib next to the wrapper in site-packages — see
# the commit message that introduced this gate.
if os.environ.get("IFCOS_BUILD_PYTHON_WRAPPER", "on").lower() in {"0", "off", "false", "no"}:
targets.discard("IfcOpenShell-Python")
if WASM:
SKIP_TARGETS_FOR_WASM = {
"hdf5",
"rocksdb",
"opencollada",
"swig",
"pcre",
"pcre2",
"IfcGeom",
"IfcConvert",
"IfcGeomServer",
"BonsaiViewer",
"qt6",
}
SKIP_TARGETS_FOR_WASM = {t.lower() for t in SKIP_TARGETS_FOR_WASM}
skip_targets = {t for t in targets if t.lower() in SKIP_TARGETS_FOR_WASM}
@@ -433,13 +434,18 @@ print("Building:", *sorted(targets, key=lambda t: len(list(gather_dependencies(t
# Check that required tools are in PATH
yacc = "yacc" # Used during swig building process, installed with `bison` on Debian / `byacc` on Red Hat.
bison = "bison"
missing_commands: "list[str]" = []
required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz]
required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz, bison]
if "wasm" in flags:
# Skip swig build for WASM.
required_commands.append("swig")
required_commands.append("pyodide")
required_commands.remove(yacc)
required_commands.remove(bison)
if platform.system() == "Linux" and "BonsaiViewer" in targets:
required_commands.append("patchelf")
for cmd in required_commands:
if shutil.which(cmd) is None:
@@ -498,7 +504,7 @@ def run(cmds: "Sequence[str]", cwd: "Union[str, None]" = None, can_fail: bool =
collector.append(line)
pipe.close()
logger.debug(f"running command {' '.join(cmds)} in directory {cwd}")
logger.debug(f"running command `{' '.join(cmds)}` in directory '{cwd}'")
stdout: list[str] = []
stderr: list[str] = []
@@ -544,14 +550,14 @@ BOOST_LOCATION = f"https://github.com/boostorg/boost/releases/download/boost-{BO
# Helper functions
def run_autoconf(arg1: str, configure_args: "list[str]", cwd: str) -> None:
def run_autoconf(dependency_name: str, configure_args: "list[str]", cwd: str) -> None:
configure_path = os.path.realpath(os.path.join(cwd, "..", "configure"))
if not os.path.exists(configure_path):
run(
[bash, "./autogen.sh"], cwd=os.path.realpath(os.path.join(cwd, ".."))
) # only run autogen.sh in the directory it is located and use cwd to achieve that in order to not mess up things
# Using `sh` over `bash` fixes issues with building swig
prefix = os.path.realpath(f"{DEPS_DIR}/install/{arg1}")
prefix = os.path.realpath(f"{DEPS_DIR}/install/{dependency_name}")
wasm = []
if "wasm" in flags:
@@ -592,6 +598,11 @@ def run_cmake(arg1, cmake_args: "list[str]", cmake_dir: Union[str, None] = None,
]
)
if not any("BUILD_SHARED_LIBS" in f for f in cmake_args):
cmake_flags.append(
f"-DBUILD_SHARED_LIBS={OFF_ON[not BUILD_STATIC]}",
)
run(
[
*wasm,
@@ -600,7 +611,6 @@ def run_cmake(arg1, cmake_args: "list[str]", cmake_dir: Union[str, None] = None,
*cmake_flags,
*cmake_args,
f"-DCMAKE_BUILD_TYPE={BUILD_CFG}",
f"-DBUILD_SHARED_LIBS={OFF_ON[not BUILD_STATIC]}",
f"-DCMAKE_SHARED_LINKER_FLAGS={os.environ['LDFLAGS']}",
],
cwd=cwd,
@@ -635,15 +645,15 @@ def build_dependency(
mode: Literal[
"cmake",
"autoconf",
"ctest",
"bjam",
],
build_tool_args: "list[str]",
download_url: str,
download_name: str,
*,
download_tool: Literal["py", "git"] = download_tool_default,
revision: "Union[str, None]" = None,
patch: "Union[str, list[str], None]" = None,
patch: list[str] | None = None,
shell=None,
pre_compile_subs: "Sequence[tuple[str, str, str]]" = (),
additional_files: "Union[dict[str, str], None]" = None,
@@ -710,7 +720,10 @@ def build_dependency(
compr = "xz"
else:
raise RuntimeError("fix source for new download type")
download_tarfile = tarfile.open(name=download_tarfile_path, mode=f"r:{compr}")
# ty: false positive bug upstream.
download_tarfile = tarfile.open(
name=download_tarfile_path, mode=f"r:{compr}"
) # ty:ignore[no-matching-overload]
# tarfile seriously doesn't have a function to retrieve the root directory more easily
extract_dir_name = os.path.commonprefix([x for x in download_tarfile.getnames() if x != "."])
# run([tar, "--exclude=\"*/*\"", "-tf", download_name], cwd=build_dir).strip() no longer works
@@ -728,8 +741,6 @@ def build_dependency(
urlretrieve(url, os.path.join(extract_dir, path))
if patch is not None:
if isinstance(patch, str):
patch = [patch]
for p in patch:
patch_abs = (SCRIPT_PATH / p).absolute().__str__()
if os.path.exists(patch_abs):
@@ -738,27 +749,13 @@ def build_dependency(
except Exception as e:
# Assert that the patch has already been applied
run(["patch", "-p1", "--batch", "--reverse", "--dry-run", "-i", patch_abs], cwd=extract_dir)
else:
raise FileNotFoundError(patch_abs)
if shell is not None:
sp.run(shell, shell=True, check=True, cwd=extract_dir)
if mode == "ctest":
try:
run(
["ctest", "-S", "HDF5config.cmake,BUILD_GENERATOR=Unix", "-C", BUILD_CFG, "-V", "-O", "hdf5.log"],
cwd=extract_dir,
)
except Exception as e:
print("-" * 70)
print(open(os.path.join(extract_dir, "hdf5.log")))
print("-" * 70)
raise e
run([tar, "-xf", kwargs["ctest_result"] + ".tar.gz"], cwd=os.path.join(extract_dir, "build"))
shutil.copytree(
os.path.join(extract_dir, "build", kwargs["ctest_result"], kwargs["ctest_result_path"]),
os.path.join(DEPS_DIR, "install", name),
)
elif mode != "bjam":
if mode != "bjam":
extract_build_dir = os.path.join(extract_dir, *([cmake_dir] if cmake_dir else []), "build")
if os.path.exists(extract_build_dir):
shutil.rmtree(extract_build_dir)
@@ -797,6 +794,85 @@ def build_dependency(
shutil.rmtree(build_dir, ignore_errors=True)
def get_qt6_aqt_config() -> "tuple[str, str, str]":
if platform.system() != "Linux":
raise ValueError("Automatic Qt6 installation with aqtinstall is only configured for Linux builds.")
machine = platform.machine().lower()
if machine in {"x86_64", "amd64"}:
return "linux", "linux_gcc_64", "gcc_64"
if machine in {"aarch64", "arm64"}:
return "linux_arm64", "linux_gcc_arm64", "gcc_arm64"
raise ValueError(f"Automatic Qt6 installation is not configured for architecture '{platform.machine()}'.")
def install_qt6() -> str:
# If the caller pre-set QT_DIR (e.g. macOS CI using Homebrew-installed
# Qt6), validate it points at a real Qt6 install and skip aqtinstall
# entirely. The aqt download is only wired for Linux; on macOS/Windows
# the supported flow is a pre-installed Qt6 advertised via QT_DIR.
preset_qt_dir = os.environ.get("QT_DIR", "").strip()
if preset_qt_dir:
preset_qt_config = Path(preset_qt_dir) / "lib" / "cmake" / "Qt6" / "Qt6Config.cmake"
if preset_qt_config.exists():
logger.info(f"Using pre-set QT_DIR={preset_qt_dir}, skipping aqt install")
return preset_qt_dir
logger.warning(
f"QT_DIR={preset_qt_dir} is set but {preset_qt_config} not found; " f"falling through to aqtinstall"
)
host, qt_arch, install_suffix = get_qt6_aqt_config()
qt_install_root = INSTALL_DIR / f"qt6-{QT6_VERSION}-{install_suffix}"
qt_dir = qt_install_root / QT6_VERSION / install_suffix
os.environ["QT_DIR"] = str(qt_dir)
qt_config = qt_dir / "lib" / "cmake" / "Qt6" / "Qt6Config.cmake"
qt_core = qt_dir / "lib" / "libQt6Core.so.6"
qt_svg = qt_dir / "lib" / "cmake" / "Qt6Svg" / "Qt6SvgConfig.cmake"
if qt_config.exists() and qt_core.exists() and qt_svg.exists():
logger.info(f"Found existing Qt6 at {qt_dir}, skipping")
return str(qt_dir)
os.makedirs(qt_install_root, exist_ok=True)
try:
import aqt # ty:ignore[unresolved-import]
except ModuleNotFoundError:
logger.error(
"Could not find an existing Qt6 install, so aqtinstall is needed to fetch it automatically. "
"Install the `aqtinstall` PyPI package or set QT_DIR."
)
exit(1)
run(
[
sys.executable,
"-m",
"aqt",
"install-qt",
host,
"desktop",
QT6_VERSION,
qt_arch,
"-O",
str(qt_install_root),
# Keep the install lean by filtering archives: qtbase provides
# Core/Gui/Widgets (and the Qt6::CorePrivate target), qtsvg provides
# Qt6::Svg. Both are base-Qt archives, not add-on modules.
"--archives",
"icu",
"qtbase",
"qtsvg",
]
)
if not (qt_config.exists() and qt_core.exists() and qt_svg.exists()):
raise RuntimeError(f"Qt6 installation did not produce a usable Qt at {qt_dir}.")
return str(qt_dir)
cecho("Collecting dependencies:", GREEN)
# Set compiler flags for 32bit builds on 64bit system
@@ -854,37 +930,6 @@ os.environ["LDFLAGS"] = LDFLAGS
# @tfk: this is no longer needed
# build_dependency(name="cmake-%s" % (CMAKE_VERSION,), mode="autoconf", build_tool_args=[], download_url="https://cmake.org/files/v%s" % (CMAKE_VERSION_2,), download_name="cmake-%s.tar.gz" % (CMAKE_VERSION,))
if "hdf5" in targets:
# not supported
orig = [os.environ[f] for f in compiler_flags]
for f in compiler_flags:
os.environ[f] = re.sub(r"-flto(=\w+)?", "", os.environ[f])
HDF5_UNDERSCORE = "_".join(HDF5_VERSION.split("."))
HDF5_MAJOR = ".".join(HDF5_VERSION.split(".")[:-1])
dependency_name = f"hdf5-{HDF5_VERSION}"
build_dependency(
name=dependency_name,
mode="cmake",
build_tool_args=[
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/{dependency_name}",
"-DHDF5_ENABLE_Z_LIB_SUPPORT=OFF",
"-DBUILD_TESTING=OFF",
"-DHDF5_BUILD_TOOLS=OFF",
"-DHDF5_BUILD_EXAMPLES=OFF",
"-DBUILD_SHARED_LIBS=OFF",
"-DHDF5_BUILD_UTILS=OFF",
"-DHDF5_BUILD_CPP_LIB=ON",
*MAC_CROSS_COMPILE_INTEL_ARGS,
],
download_url=f"https://github.com/HDFGroup/hdf5/archive/refs/tags/",
download_name=f"hdf5-{HDF5_UNDERSCORE}.tar.gz",
)
for f, o in zip(compiler_flags, orig):
os.environ[f] = o
if "json" in targets:
dependency_name = f"json-{JSON_VERSION}"
build_dependency(
@@ -930,20 +975,15 @@ if "pcre" in targets:
restore_env("CC", OLD_CC)
restore_env("CXX", OLD_CXX)
if "pcre2" in targets:
build_dependency(
name=f"pcre2-{PCRE2_VERSION}",
mode="autoconf",
build_tool_args=[DISABLE_FLAG],
download_url=f"https://downloads.sourceforge.net/project/pcre/pcre2/{PCRE2_VERSION}/",
download_name=f"pcre2-{PCRE2_VERSION}.tar.bz2",
)
if "swig" in targets:
dependency_name = f"swig-{SWIG_VERSION}"
build_dependency(
name=f"swig-{SWIG_VERSION}",
mode="autoconf",
build_tool_args=["--disable-ccache", f"--with-pcre2-prefix={DEPS_DIR}/install/pcre2-{PCRE2_VERSION}"],
name=dependency_name,
mode="cmake",
build_tool_args=[
"-DWITH_PCRE=OFF",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/{dependency_name}",
],
download_url="https://github.com/swig/swig.git",
download_name="swig",
download_tool=download_tool_git,
@@ -951,21 +991,18 @@ if "swig" in targets:
)
if USE_OCCT and "occ" in targets:
patches = []
occt_args: "list[str]" = []
patches: "list[str]" = []
if OCCT_VERSION < "7.4":
patches.append("./patches/occt/enable-exception-handling.patch")
if OCCT_VERSION == "7.7.1":
# Skip ExpToCasExe as we don't need it and it requires additional dependencies.
# Before 7.7.2 ExpToCasExe is part of DataExchange, DETools doesn't exist yet.
# Since we do need DataExchange (used for IgesSerializer), we use a patch to skip only ExpToCasExe.
if "7.7.2" > OCCT_VERSION >= "7.7":
patches.append("./patches/occt/no_ExpToCasExe.patch")
if OCCT_VERSION == "7.7.2":
patches.append("./patches/occt/no_ExpToCasExe_7_7_2.patch")
if OCCT_VERSION == "7.8.1":
patches.append("./patches/occt/no_ExpToCasExe_7_8_1.patch")
if OCCT_VERSION == "7.9.1":
patches.append("./patches/occt/no_ExpToCasExe_7_9_1.patch")
elif OCCT_VERSION >= "7.7.2":
occt_args.append("-DBUILD_MODULE_DETools=OFF")
if "wasm" in flags:
patches.append("./patches/occt/no_em_js.patch")
@@ -986,6 +1023,7 @@ if USE_OCCT and "occ" in targets:
f"-DUSE_GLES2=OFF",
f"-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
*MAC_CROSS_COMPILE_INTEL_ARGS,
*occt_args,
],
download_url="https://github.com/Open-Cascade-SAS/OCCT",
download_name="occt",
@@ -1010,6 +1048,33 @@ elif "occ" in targets:
download_name=f"OCE-{OCE_VERSION}.tar.gz",
)
if "manifold" in targets:
dependency_name = f"manifold-{MANIFOLD_VERSION}"
patches = []
if WASM:
patches.append("./patches/manifold/install-metadata-for-emscripten.patch")
build_dependency(
name=dependency_name,
mode="cmake",
build_tool_args=[
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/{dependency_name}",
"-DMANIFOLD_PAR=OFF",
"-DMANIFOLD_CROSS_SECTION=OFF",
"-DMANIFOLD_PYBIND=OFF",
"-DMANIFOLD_JSBIND=OFF",
"-DMANIFOLD_CBIND=OFF",
"-DMANIFOLD_TEST=OFF",
"-DMANIFOLD_EXPORT=OFF",
"-DMANIFOLD_DOWNLOADS=OFF",
*MAC_CROSS_COMPILE_INTEL_ARGS,
],
download_url="https://github.com/elalish/manifold.git",
download_name="manifold",
download_tool=download_tool_git,
revision=f"v{MANIFOLD_VERSION}",
patch=patches,
)
if "libxml2" in targets:
OLD_CC = ""
if MAC_CROSS_COMPILE_INTEL:
@@ -1101,23 +1166,28 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
PYTHON_CONFIGURE_ARGS.extend(["--with-universal-archs=intel-64", "--enable-universalsdk"])
for PYTHON_VERSION in PYTHON_VERSIONS:
# Don't fail silently on missing Python dependencies (e.g. openssl or zlib),
# because later ifcopenshell-python build will fail too but in a more confusing way.
build_dependency(
f"python-{PYTHON_VERSION}",
"autoconf",
PYTHON_CONFIGURE_ARGS,
f"http://www.python.org/ftp/python/{PYTHON_VERSION}/",
f"Python-{PYTHON_VERSION}.tgz",
)
python_install = INSTALL_DIR / f"python-{PYTHON_VERSION}"
python_bin = python_install / "bin" / "python3"
# `_ssl` module is present -> we will be able to install `numpy` later
# to verify IfcOpenShell installation
try:
build_dependency(
f"python-{PYTHON_VERSION}",
"autoconf",
PYTHON_CONFIGURE_ARGS,
f"http://www.python.org/ftp/python/{PYTHON_VERSION}/",
f"Python-{PYTHON_VERSION}.tgz",
run([str(python_bin), "-c", "import _ssl"])
except RuntimeError:
print(
"ERROR: Python was built without SSL support (_ssl module is missing). "
f"To fix this: remove the installed Python at {python_install}; "
"install OpenSSL development libraries and re-run."
)
except RuntimeError as e:
# Sometimes setting up modules such as pip/lzma can cause
# the python installer script to return a non zero exit
# code where actually the headers and dynamic libraries
# are installed correctly. This is all we need so we catch
# the exception and only reraise if a partially successful
# install is not detected.
if not os.path.exists(os.path.join(DEPS_DIR, "install", f"python-{PYTHON_VERSION}")):
raise e
raise
if MAC_CROSS_COMPILE_INTEL:
assert original_path
@@ -1187,6 +1257,14 @@ if "cgal" in targets:
os.environ["CC"] = MAC_CROSS_COMPILE_INTEL_CC
gmp_args.extend(MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS)
# Fixes configure failing to find a working compiler under GCC 15's default -std=gnu23.
# Issue presumably will be resolved in any next gmp version, but currently the last one is 6.3.0.
# Patch is just applying fix from upstream meantion below:
# https://gmplib.org/list-archives/gmp-bugs/2025-February/005561.html
gmp_patches = ["./patches/gmp/001-fix-std23.patch"]
if GMP_VERSION != "6.3.0":
raise Exception(f"GMP_VERSION changed to {GMP_VERSION}, check whether {gmp_patches} is still needed.")
build_dependency(
name=f"gmp-{GMP_VERSION}",
mode="autoconf",
@@ -1194,6 +1272,7 @@ if "cgal" in targets:
pre_compile_subs=(
[("build/config.h", "HAVE_OBSTACK_VPRINTF 1", "HAVE_OBSTACK_VPRINTF 0")] if "wasm" in flags else []
),
patch=gmp_patches,
# Sometimes ftp.gnu.org is very slow, use ftpmirror.gnu.org as a workaround.
download_url="https://ftpmirror.gnu.org/gnu/gmp/",
download_name=f"gmp-{GMP_VERSION}.tar.bz2",
@@ -1308,6 +1387,9 @@ if "rocksdb" in targets:
revision=f"v{ROCKSDB_VERSION}",
)
if "qt6" in targets:
install_qt6()
cecho("Building IfcOpenShell:", GREEN)
IFCOS_DIR = os.path.join(DEPS_DIR, "build", "ifcopenshell")
@@ -1315,8 +1397,8 @@ if os.environ.get("NO_CLEAN", "").lower() not in {"1", "on", "true"}:
if os.path.exists(IFCOS_DIR):
shutil.rmtree(IFCOS_DIR)
os.makedirs(IFCOS_DIR, exist_ok=True)
executables_dir = os.path.join(IFCOS_DIR, "executables")
os.makedirs(executables_dir, exist_ok=True)
ifcos_build_dir = os.path.join(IFCOS_DIR, "build")
os.makedirs(ifcos_build_dir, exist_ok=True)
cmake_args = [
@@ -1325,6 +1407,7 @@ cmake_args = [
"-DBUILD_SHARED_LIBS=" + OFF_ON[not BUILD_STATIC],
"-DGLTF_SUPPORT=ON",
"-DBoost_NO_BOOST_CMAKE=On",
"-DCREATE_BUNDLE=On",
"-DADD_COMMIT_SHA=" + ("On" if ADD_COMMIT_SHA else "Off"),
"-DVERSION_OVERRIDE=" + ("On" if ADD_COMMIT_SHA else "Off"),
*MAC_CROSS_COMPILE_INTEL_ARGS,
@@ -1374,6 +1457,10 @@ elif "occ" in targets:
occ_library_dir = f"{DEPS_DIR}/install/oce-{OCE_VERSION}/lib"
cmake_args.extend(["-DOCC_INCLUDE_DIR=" + occ_include_dir, "-DOCC_LIBRARY_DIR=" + occ_library_dir])
if "manifold" in targets:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/manifold-{MANIFOLD_VERSION}")
cmake_args.append("-DWITH_MANIFOLD=On")
if "OpenCOLLADA" in targets:
# pcre is a dependency of OpenCOLLADA, but since we `find_package`,
# we don't need to add it explicitly here as cmake will find it from the config.
@@ -1388,11 +1475,6 @@ else:
if "libxml2" in targets:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/libxml2-{LIBXML2_VERSION}")
if "hdf5" in targets:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/hdf5-{HDF5_VERSION}")
else:
cmake_args.append("-DHDF5_SUPPORT=Off")
if "usd" in targets:
cmake_args.append("-DUSD_SUPPORT=ON")
cmake_args_prefix_path.extend(
@@ -1419,40 +1501,44 @@ if "rocksdb" in targets:
if "swig" in targets:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/swig-{SWIG_VERSION}")
if not WASM and (not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets)):
if os.environ.get("QT_DIR"):
cmake_args_prefix_path.append(os.environ["QT_DIR"])
cmake_args.append(f"-DQT_DIR={os.environ['QT_DIR']}")
build_bonsaiviewer = BUILD_BONSAIVIEWER or "BonsaiViewer" in targets
ifcos_build_args = [
f"-DBUILD_IFCGEOM={OFF_ON['IfcGeom' in targets]}",
f"-DBUILD_GEOMSERVER={OFF_ON['IfcGeomServer' in targets]}",
f"-DBUILD_CONVERT={OFF_ON['IfcConvert' in targets]}",
f"-DBUILD_BONSAIVIEWER={OFF_ON[build_bonsaiviewer]}",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell",
]
if not WASM and (
build_bonsaiviewer
or not explicit_targets
or {"IfcGeom", "IfcConvert", "IfcGeomServer", "BonsaiViewer"} & set(explicit_targets)
):
logger.info("\rConfiguring executables...")
exec_args = [
f"-DBUILD_IFCGEOM={OFF_ON['IfcGeom' in targets]}",
f"-DBUILD_GEOMSERVER={OFF_ON['IfcGeomServer' in targets]}",
f"-DBUILD_CONVERT={OFF_ON['IfcConvert' in targets]}",
*ifcos_build_args,
f"-DBUILD_IFCPYTHON=OFF",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell",
]
run_cmake("", exec_args + cmake_args + get_cmake_args_prefix_path(), cmake_dir=CMAKE_DIR, cwd=executables_dir)
run_cmake("", exec_args + cmake_args + get_cmake_args_prefix_path(), cmake_dir=CMAKE_DIR, cwd=ifcos_build_dir)
logger.info("\rBuilding executables... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=executables_dir)
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=executables_dir)
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=ifcos_build_dir)
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=ifcos_build_dir)
if "IfcOpenShell-Python" in targets:
# On OSX the actual Python library is not linked against.
ADDITIONAL_ARGS = ""
wrapper_ldflags = ""
if platform.system() == "Darwin":
ADDITIONAL_ARGS = "-Wl,-undefined,dynamic_lookup"
# NOTE: We don't use `CXXFLAGS` for wrappers, so wrapper is compiled with different flags
# (e.g. ` -fdata-sections` is missing, which is set by default for executables)
# So cache doesn't match and running build-all.py builds most of ifcopenshell libraries twice.
os.environ["CPPFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CXXFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CFLAGS"] = f"{CFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["LDFLAGS"] = f"{LDFLAGS} {ADDITIONAL_ARGS}"
python_dir = os.path.join(IFCOS_DIR, "pythonwrapper")
os.makedirs(python_dir, exist_ok=True)
# On OSX the actual Python library is not linked against.
wrapper_ldflags = "-Wl,-undefined,dynamic_lookup"
def compile_python_wrapper(
python_version: str,
@@ -1467,10 +1553,6 @@ if "IfcOpenShell-Python" in targets:
logger.info(f"\rConfiguring python {python_version} wrapper...")
cache_path = os.path.join(python_dir, "CMakeCache.txt")
if os.path.exists(cache_path):
os.remove(cache_path)
if python_path:
# We couldn't just prefix PATH and have to provide all variables explicitly,
# see ifcwrap/cmake for the details.
@@ -1484,27 +1566,38 @@ if "IfcOpenShell-Python" in targets:
)
assert python_include
run_cmake(
"",
cmake_args
+ get_cmake_args_prefix_path()
+ [
*([f"-DPYTHON_EXECUTABLE={python_executable}"] if python_executable else []),
# Needed because pyodide is expecting setup.py to be in the root.
*([f"-DPYTHON_MODULE_INSTALL_DIR={REPO_PATH}"] * WASM),
f"-DPYTHON_INCLUDE_DIR={python_include}",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell/tmp",
"-DUSERSPACE_PYTHON_PREFIX="
+ ["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
],
cmake_dir=CMAKE_DIR,
cwd=python_dir,
)
old_ldflags = os.environ["LDFLAGS"]
if wrapper_ldflags:
os.environ["LDFLAGS"] = f"{old_ldflags} {wrapper_ldflags}"
try:
run_cmake(
"",
ifcos_build_args
+ [
"-DBUILD_IFCPYTHON=ON",
]
+ cmake_args
+ get_cmake_args_prefix_path()
+ [
*([f"-DPYTHON_EXECUTABLE={python_executable}"] if python_executable else []),
# Needed because pyodide is expecting setup.py to be in the root.
*([f"-DPYTHON_MODULE_INSTALL_DIR={REPO_PATH}"] * WASM),
f"-DPYTHON_INCLUDE_DIR={python_include}",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell/tmp",
"-DUSERSPACE_PYTHON_PREFIX="
+ ["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
],
cmake_dir=CMAKE_DIR,
cwd=ifcos_build_dir,
)
finally:
os.environ["LDFLAGS"] = old_ldflags
logger.info(f"\rBuilding python {python_version} wrapper... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper", "VERBOSE=1"], cwd=python_dir)
run([make, "install/local"], cwd=os.path.join(python_dir, "ifcwrap"))
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper", "VERBOSE=1"], cwd=ifcos_build_dir)
run([make, "install/local"], cwd=os.path.join(ifcos_build_dir, "ifcwrap"))
if python_executable:
run([python_executable, "-m", "ensurepip"])
@@ -1519,12 +1612,14 @@ if "IfcOpenShell-Python" in targets:
if platform.system() != "Darwin":
if BUILD_CFG == "Release":
# TODO: This symbol name depends on the Python version?
so = glob.glob(os.path.join(module_dir, "_ifcopenshell_wrapper*.so"))[0]
if "wasm" in flags:
run(["wasm-strip", so, "-k", "dylink.0"])
else:
run([strip, "-s", "-K", "PyInit__ifcopenshell_wrapper", so], cwd=module_dir)
for so in glob.glob(os.path.join(module_dir, "*.so")):
if "wasm" in flags:
run(["wasm-strip", so, "-k", "dylink.0"])
elif os.path.basename(so).startswith("_ifcopenshell_wrapper"):
# TODO: This symbol name depends on the Python version?
run([strip, "-s", "-K", "PyInit__ifcopenshell_wrapper", so], cwd=module_dir)
else:
run([strip, "--strip-unneeded", so], cwd=module_dir)
return module_dir
@@ -1535,19 +1630,25 @@ if "IfcOpenShell-Python" in targets:
)
# Copy setup.py where pyodide build system expects it.
shutil.copy(REPO_PATH / "pyodide" / "setup.py", REPO_PATH)
# Empty pyproject so it's contents won't affect the resulting wheel
# otherwise the wheel will use version and dependencies from toml, not setup.py.
(REPO_PATH / "pyproject.toml").write_text("")
elif USE_CURRENT_PYTHON_VERSION:
python_info = sysconfig.get_paths()
compile_python_wrapper(platform.python_version(), python_info["include"], sys.executable)
else:
for python_version in PYTHON_VERSIONS:
python_path = Path(DEPS_DIR) / "install" / f"python-{python_version}"
python_path = INSTALL_DIR / f"python-{python_version}"
module_dir = compile_python_wrapper(python_version, python_path=python_path)
assert module_dir
# Not sure why, but added after reading this in the logs
# cp: /Users/runner/work/IfcOpenShell/IfcOpenShell/build/Darwin/x86_64/10.15/install/ifcopenshell/python-3.9.11: No such file or directory
# D'oh this was just due to a missing f-string f but doesn't hurt to keep it in.
run(["mkdir", "-p", os.path.join(DEPS_DIR, "install", "ifcopenshell")])
run([cp, "-R", module_dir, os.path.join(DEPS_DIR, "install", "ifcopenshell", f"python-{python_version}")])
dest = os.path.join(DEPS_DIR, "install", "ifcopenshell", f"python-{python_version}")
if os.path.exists(dest):
shutil.rmtree(dest)
run([cp, "-R", module_dir, dest])
logger.info("\rBuilt IfcOpenShell...\n\n")
@@ -1,3 +1,5 @@
# /// script
# ///
"""
Cache built dependencies for builds.
@@ -5,9 +7,13 @@ This script is finding common install directory and either
packs each folder into a tar.gz archive, if it wasn't packed before,
or unpacks existing archives.
Expected to be executed from 'build' directory (e.g. that might contain 'Linux/x86_64/install').
Usage: python cache_dependencies.py [pack|unpack]
"""
import platform
import subprocess
import sys
import tarfile
from pathlib import Path
@@ -17,23 +23,35 @@ CACHE_PREFIX = "cache-"
def get_install_dir() -> Path:
for data in Path.cwd().glob("*/*/install"):
if platform.system() == "Darwin":
pattern = "Darwin/*/*/install"
else:
pattern = "*/*/install"
for data in Path.cwd().glob(pattern):
return data
raise Exception("No install dir found")
def run(cmd: str) -> None:
print(f"Running command: `{cmd}`")
subprocess.check_call(cmd, shell=True)
def pack_dependencies(install_dir: Path) -> None:
# Process each install_dir
for dependency_path in install_dir.iterdir():
if not dependency_path.is_dir():
continue
dependency_name = dependency_path.name
# Skip ifcopenshell - it's a build output, not a dependency to reuse across builds.
if dependency_name == "ifcopenshell":
continue
tar_path = install_dir / f"{CACHE_PREFIX}{dependency_name}.tar.gz"
if tar_path.exists():
print(f"Skipping existing cache: '{tar_path}'")
else:
with tarfile.open(tar_path, "w:gz") as tar:
tar.add(dependency_path, arcname=dependency_path.name)
# Python's `tarfile` is 10x slower than `tar` cli, so we use `tar`.
run(f'tar -czf "{tar_path}" -C "{install_dir}" "{dependency_name}"')
print(f"Created cache: '{tar_path}'")
+27
View File
@@ -0,0 +1,27 @@
Fixes configure failing to find a working compiler under GCC 15's default
-std=gnu23 (upstream fix: https://gmplib.org/repo/gmp/rev/8e7bb4ae7a18).
Upstream fix is patching `acinclude.m4`, but since in the release tarball
all macros are already expanded to `configure` script, so we're patching
all occurrences of that macro.
--- a/configure
+++ b/configure
@@ -6568,7 +6568,7 @@
#if defined (__GNUC__) && ! defined (__cplusplus)
typedef unsigned long long t1;typedef t1*t2;
-void g(){}
+void g(int,t1 const*,t1,t2,t1 const*,int){}
void h(){}
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
@@ -8187,7 +8187,7 @@
#if defined (__GNUC__) && ! defined (__cplusplus)
typedef unsigned long long t1;typedef t1*t2;
-void g(){}
+void g(int,t1 const*,t1,t2,t1 const*,int){}
void h(){}
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
@@ -0,0 +1,17 @@
# This file was generated with the assistance of an AI coding tool.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 42e403b..764562f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -249,11 +249,6 @@ set_source_files_properties(
PROPERTIES GENERATED TRUE
)
-# If it's an EMSCRIPTEN build, we're done
-if(EMSCRIPTEN)
- return()
-endif()
-
# CMake exports
configure_file(
cmake/manifoldConfig.cmake.in
-32
View File
@@ -1,32 +0,0 @@
http://git.dev.opencascade.org/gitweb/?p=occt.git;a=commitdiff;h=0ab4e621833f4eae945a3762c9a29ee12e2eec53#patch1
diff --git a/src/HLRBRep/HLRBRep_InternalAlgo.cxx b/src/HLRBRep/HLRBRep_InternalAlgo.cxx
index ca885ca..c13cb06 100644 (file)
--- a/src/HLRBRep/HLRBRep_InternalAlgo.cxx
+++ b/src/HLRBRep/HLRBRep_InternalAlgo.cxx
@@ -165,7 +165,7 @@ void HLRBRep_InternalAlgo::Update ()
SB.Bounds(v1,v2,e1,e2,f1,f2);
for (Standard_Integer e = e1; e <= e2; e++) {
- HLRBRep_EdgeData ed = aEDataArray.ChangeValue(e);
+ HLRBRep_EdgeData& ed = aEDataArray.ChangeValue(e);
HLRAlgo::DecodeMinMax(ed.MinMax(), TheMin, TheMax);
if (FirstTime) {
FirstTime = Standard_False;
@@ -307,7 +307,7 @@ void HLRBRep_InternalAlgo::InitEdgeStatus ()
Standard_Integer nf = myDS->NbFaces();
for (Standard_Integer e = 1; e <= ne; e++) {
- HLRBRep_EdgeData ed = aEDataArray.ChangeValue(e);
+ HLRBRep_EdgeData& ed = aEDataArray.ChangeValue(e);
if (ed.Selected()) ed.Status().ShowAll();
}
// for (Standard_Integer f = 1; f <= nf; f++) {
@@ -368,7 +368,7 @@ void HLRBRep_InternalAlgo::Select ()
Standard_Integer nf = myDS->NbFaces();
for (Standard_Integer e = 1; e <= ne; e++) {
- HLRBRep_EdgeData ed = aEDataArray.ChangeValue(e);
+ HLRBRep_EdgeData& ed = aEDataArray.ChangeValue(e);
ed.Selected(Standard_True);
}
+9 -13
View File
@@ -1,13 +1,9 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fd17283f77..6cecf9dad3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -826,6 +826,8 @@ if (EMSCRIPTEN)
list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe)
endif()
+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe)
+
# bison
if (BUILD_YACCLEX)
OCCT_INCLUDE_CMAKE_FILE ("adm/cmake/bison")
--- a/adm/MODULES
+++ b/adm/MODULES
@@ -3,5 +3,5 @@ ModelingData TKG2d TKG3d TKGeomBase TKBRep
ModelingAlgorithms TKGeomAlgo TKTopAlgo TKPrim TKBO TKBool TKHLR TKFillet TKOffset TKFeat TKMesh TKXMesh TKShHealing
Visualization TKService TKV3d TKOpenGl TKOpenGles TKMeshVS TKIVtk TKD3DHost
ApplicationFramework TKCDF TKLCAF TKCAF TKBinL TKXmlL TKBin TKXml TKStdL TKStd TKTObj TKBinTObj TKXmlTObj TKVCAF
-DataExchange TKXDE TKXSBase TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP TKIGES TKXCAF TKXDEIGES TKXDESTEP TKSTL TKVRML TKXmlXCAF TKBinXCAF TKRWMesh TKXDECascade TKExpress ExpToCasExe
+DataExchange TKXDE TKXSBase TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP TKIGES TKXCAF TKXDEIGES TKXDESTEP TKSTL TKVRML TKXmlXCAF TKBinXCAF TKRWMesh TKXDECascade TKExpress
Draw TKDraw TKTopTest TKOpenGlTest TKOpenGlesTest TKD3DHostTest TKViewerTest TKXSDRAW TKDCAF TKXDEDRAW TKTObjDRAW TKQADraw TKIVtkDraw DRAWEXE
@@ -1,13 +0,0 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1bacca1a48..11f931ad39 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -820,6 +820,8 @@ else()
OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE")
endif()
+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe)
+
# bison
if (BUILD_YACCLEX)
OCCT_INCLUDE_CMAKE_FILE ("adm/cmake/bison")
@@ -1,13 +0,0 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 86905287dc..9d0bce984c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -828,6 +828,8 @@ else()
OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE")
endif()
+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe)
+
# bison
if (BUILD_YACCLEX)
OCCT_INCLUDE_CMAKE_FILE ("adm/cmake/bison")
@@ -1,13 +0,0 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 34300d41ad..09b2e0d45f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -721,6 +721,8 @@ else()
OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE")
endif()
+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe)
+
# bison
if (BUILD_YACCLEX)
list (APPEND OCCT_3RDPARTY_CMAKE_LIST "adm/cmake/bison")
-22
View File
@@ -1,22 +0,0 @@
From a0deb4ce8b43cf3c8b8c0a4225c6be5296446dbd Mon Sep 17 00:00:00 2001
From: Adam Eri <adam.eri@blackmirror.media>
Date: Tue, 3 Sep 2019 23:30:20 +0200
Subject: [PATCH] Resolves compile error on macOS
Resolves "no member named 'isnan' in namespace 'std'" on macOS
---
GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp | 1 +
1 file changed, 1 insertion(+)
diff --git a/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp b/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp
index 1f9a3eef..dd6f5c59 100644
--- a/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp
+++ b/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp
@@ -10,6 +10,7 @@
#include "GeneratedSaxParserUtils.h"
#include <math.h>
+#include <cmath>
#include <memory>
#include <string.h>
#include <limits>
-112
View File
@@ -34,7 +34,6 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx-13.3.0-h9576a4e_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-13.3.0-hdbfa832_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-13.3.0-h6834431_7.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.4-nompi_h2d575fe_105.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/imath-3.1.12-h7955e40_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/jxrlib-1.1-hd590300_3.conda
@@ -149,7 +148,6 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/freeimage-3.18.0-h7cd8ba8_22.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.12.1-h60636b9_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.4-nompi_h1607680_105.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/imath-3.1.12-h2016aa1_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/jxrlib-1.1-h10d778d_3.conda
@@ -243,7 +241,6 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/win-64/freeimage-3.18.0-h8310ca0_22.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.12.1-hdaf720e_2.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/gmp-6.3.0-hfeafd45_2.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.4-nompi_hd5d9e70_105.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/imath-3.1.12-hbb528cf_0.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/jxrlib-1.1-hcfcfb64_3.conda
@@ -349,7 +346,6 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx-13.3.0-h9576a4e_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-13.3.0-hdbfa832_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-13.3.0-h6834431_7.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.4-nompi_h2d575fe_105.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/imath-3.1.12-h7955e40_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/jxrlib-1.1-hd590300_3.conda
@@ -464,7 +460,6 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/freeimage-3.18.0-h7cd8ba8_22.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.12.1-h60636b9_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.4-nompi_h1607680_105.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/imath-3.1.12-h2016aa1_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/jxrlib-1.1-h10d778d_3.conda
@@ -558,7 +553,6 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/win-64/freeimage-3.18.0-h8310ca0_22.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.12.1-hdaf720e_2.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/gmp-6.3.0-hfeafd45_2.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.4-nompi_hd5d9e70_105.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/imath-3.1.12-hbb528cf_0.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/jxrlib-1.1-hcfcfb64_3.conda
@@ -743,7 +737,6 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx-13.3.0-h9576a4e_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-13.3.0-hdbfa832_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-13.3.0-h6834431_7.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.4-nompi_h2d575fe_105.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/imath-3.1.12-h7955e40_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/jxrlib-1.1-hd590300_3.conda
@@ -858,7 +851,6 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/freeimage-3.18.0-h7cd8ba8_22.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.12.1-h60636b9_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.4-nompi_h1607680_105.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/imath-3.1.12-h2016aa1_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/jxrlib-1.1-h10d778d_3.conda
@@ -952,7 +944,6 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/win-64/freeimage-3.18.0-h8310ca0_22.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.12.1-hdaf720e_2.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/gmp-6.3.0-hfeafd45_2.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.4-nompi_hd5d9e70_105.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/imath-3.1.12-hbb528cf_0.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/jxrlib-1.1-hcfcfb64_3.conda
@@ -1068,7 +1059,6 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-14.3.0-he663afc_4.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-14.3.0-ha7acb78_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h6e4c0c1_103.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda
@@ -1239,7 +1229,6 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/geos-3.13.1-h502464c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.6-nompi_hc8237f9_103.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda
@@ -1380,7 +1369,6 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/win-64/geos-3.13.1-h9ea8674_0.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/gmp-6.3.0-hfeafd45_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.6-nompi_he30205f_103.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda
@@ -2990,106 +2978,6 @@ packages:
- pkg:pypi/h2?source=compressed-mapping
size: 95967
timestamp: 1756364871835
- conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.4-nompi_h2d575fe_105.conda
sha256: 93d2bfc672f3ee0988d277ce463330a467f3686d3f7ee37812a3d8ca11776d77
md5: d76fff0092b6389a12134ddebc0929bd
depends:
- __glibc >=2.17,<3.0.a0
- libaec >=1.1.3,<2.0a0
- libcurl >=8.10.1,<9.0a0
- libgcc >=13
- libgfortran
- libgfortran5 >=13.3.0
- libstdcxx >=13
- libzlib >=1.3.1,<2.0a0
- openssl >=3.4.0,<4.0a0
license: BSD-3-Clause
license_family: BSD
size: 3950601
timestamp: 1733003331788
- conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h6e4c0c1_103.conda
sha256: 4f173af9e2299de7eee1af3d79e851bca28ee71e7426b377e841648b51d48614
md5: c74d83614aec66227ae5199d98852aaf
depends:
- __glibc >=2.17,<3.0.a0
- libaec >=1.1.4,<2.0a0
- libcurl >=8.14.1,<9.0a0
- libgcc >=14
- libgfortran
- libgfortran5 >=14.3.0
- libstdcxx >=14
- libzlib >=1.3.1,<2.0a0
- openssl >=3.5.1,<4.0a0
license: BSD-3-Clause
license_family: BSD
purls: []
size: 3710057
timestamp: 1753357500665
- conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.4-nompi_h1607680_105.conda
sha256: 56500937894b1ca917e1ae1bea64b873a9eec57d581173579189d0b1f590db26
md5: 12ebafc40b10d4bf519e4c2074c52aef
depends:
- __osx >=10.13
- libaec >=1.1.3,<2.0a0
- libcurl >=8.10.1,<9.0a0
- libcxx >=18
- libgfortran 5.*
- libgfortran5 >=13.2.0
- libzlib >=1.3.1,<2.0a0
- openssl >=3.4.0,<4.0a0
license: BSD-3-Clause
license_family: BSD
size: 3732340
timestamp: 1733003702265
- conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.6-nompi_hc8237f9_103.conda
sha256: e41d22f672b1fbe713d22cf69630abffaee68bdb38a500a708fc70e6f639357f
md5: 3f1df98f96e0c369d94232712c9b87d0
depends:
- __osx >=10.13
- libaec >=1.1.4,<2.0a0
- libcurl >=8.14.1,<9.0a0
- libcxx >=19
- libgfortran
- libgfortran5 >=14.3.0
- libgfortran5 >=15.1.0
- libzlib >=1.3.1,<2.0a0
- openssl >=3.5.1,<4.0a0
license: BSD-3-Clause
license_family: BSD
purls: []
size: 3522832
timestamp: 1753358062940
- conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.4-nompi_hd5d9e70_105.conda
sha256: e8ced65c604a3b9e4803758a25149d71d8096f186fe876817a0d1d97190550c0
md5: 4381be33460283890c34341ecfa42d97
depends:
- libaec >=1.1.3,<2.0a0
- libcurl >=8.10.1,<9.0a0
- libzlib >=1.3.1,<2.0a0
- openssl >=3.4.0,<4.0a0
- ucrt >=10.0.20348.0
- vc >=14.2,<15
- vc14_runtime >=14.29.30139
license: BSD-3-Clause
license_family: BSD
size: 2048450
timestamp: 1733003052575
- conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.6-nompi_he30205f_103.conda
sha256: 0a90263b97e9860cec6c2540160ff1a1fff2a609b3d96452f8716ae63489dac5
md5: f1f7aaf642cefd2190582550eaca4658
depends:
- libaec >=1.1.4,<2.0a0
- libcurl >=8.14.1,<9.0a0
- libzlib >=1.3.1,<2.0a0
- openssl >=3.5.1,<4.0a0
- ucrt >=10.0.20348.0
- vc >=14.3,<15
- vc14_runtime >=14.44.35208
license: BSD-3-Clause
license_family: BSD
purls: []
size: 2031491
timestamp: 1753357255237
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba
md5: 0a802cb9888dd14eeefc611f05c40b6e
-1
View File
@@ -31,7 +31,6 @@ occt = { version = "*", build = "*novtk*" }
cgal-cpp = "*"
numpy = "*"
lark = "*"
hdf5 = "*"
eigen = "*"
mpfr = "*"
gmp = "*"
+22 -11
View File
@@ -1,26 +1,31 @@
#!/usr/bin/bash
set -ex
PYODIDE_VERSION=0.29.3
PYODIDE_BUILD_VERSION=0.33.0
PYODIDE_XBUILDENV_ROOT="${HOME}/.cache/.pyodide-xbuildenv-${PYODIDE_BUILD_VERSION}"
PYODIDE_XBUILDENV="${PYODIDE_XBUILDENV_ROOT}/${PYODIDE_VERSION}"
# Script is assuming that it will be possible to execute it multiple times
# therefore we're clearing venv each time and ignoring existing 'emsdk' folder.
# Install uv.
curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv --python 3.13
uv venv --python 3.13 --clear
source .venv/bin/activate
# Install pyodide cross build environment.
# Instructions: https://pyodide.org/en/stable/development/building-packages.html
uv pip install pyodide-build
uv pip install "pyodide-build==${PYODIDE_BUILD_VERSION}"
# `uv run` is required, so xbuildenv would skip using `pip`.
uv run pyodide xbuildenv install
uv run pyodide xbuildenv install "${PYODIDE_VERSION}"
uv run pyodide xbuildenv install-emscripten
# Emscripten doesn't come with xbuildenv.
git clone https://github.com/emscripten-core/emsdk
pushd emsdk
PYODIDE_EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version)
./emsdk install ${PYODIDE_EMSCRIPTEN_VERSION}
./emsdk activate ${PYODIDE_EMSCRIPTEN_VERSION}
source emsdk_env.sh
EMSDK_ROOT="${PYODIDE_XBUILDENV}/emsdk"
[ -f "${EMSDK_ROOT}/emsdk_env.sh" ] && source "${EMSDK_ROOT}/emsdk_env.sh"
[ -f "${EMSDK_ROOT}/../../emsdk_env.sh" ] && source "${EMSDK_ROOT}/../../emsdk_env.sh"
which emcc
popd
emcc --version
mkdir -p packages/ifcopenshell
VERSION=`cat IfcOpenShell/VERSION`
@@ -31,6 +36,12 @@ sed -i s/0.8.0/$VERSION/g packages/ifcopenshell/meta.yaml
# Otherwise pyodide build path typically includes package version, so cached cmake configs might break.
export BUILD_DIR=`readlink -f ifcopenshell_build`
# Sat, 25 Apr 2026 12:11:39 GMT 2026-04-25 12:11:39,173 - DEBUG - running
# command `make -j5 ifcopenshell_wrapper VERBOSE=1` in directory
# '/home/runner/work/IfcOpenShell/IfcOpenShell/ifcopenshell_build/Linux/wasm/build/ifcopenshell/build'
# Sat, 25 Apr 2026 12:18:01 GMT Error: Process completed with exit code 143.
export IFCOS_NUM_BUILD_PROCS=1
# Use build-recipes-no-deps first, so logs would be printed to stdout.
pyodide build-recipes-no-deps ifcopenshell
pyodide build-recipes ifcopenshell --install
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
# This file was generated with the assistance of an AI coding tool.
"""Order Pyodide wheel shared objects so wasm side modules load safely."""
from __future__ import annotations
import argparse
import os
import re
import tempfile
import zipfile
from pathlib import Path
SCHEMA_ORDER = {
"ifc2x3": 0,
"ifc4": 1,
"ifc4x1": 2,
"ifc4x2": 3,
"ifc4x3": 4,
"ifc4x3_add1": 5,
"ifc4x3_add2": 6,
}
MAIN_SHARED_OBJECT_RE = re.compile(r"^_ifcopenshell_wrapper(?:\.|$)")
SCHEMA_PLUGIN_RE = re.compile(r"^ifcopenshell\.parse\.schema\.([^.]+)\.so$")
MAPPING_PLUGIN_RE = re.compile(r"^ifcopenshell\.geometry\.mapping\.([^.]+)\.so$")
DOCUMENT_PLUGIN_RE = re.compile(r"^ifcopenshell\.document\.[^.]+\.([^.]+)\.so$")
GEOMETRY_SERIALIZATION_PLUGIN_RE = re.compile(r"^ifcopenshell\.geometry\.serialization\.([^.]+)\.so$")
def schema_key(schema: str) -> tuple[int, str]:
schema = schema.lower()
return SCHEMA_ORDER.get(schema, len(SCHEMA_ORDER)), schema
def shared_object_sort_key(filename: str, index: int) -> tuple[int, tuple[int, str], str, int]:
basename = Path(filename).name
if MAIN_SHARED_OBJECT_RE.match(basename):
return 0, schema_key(""), basename, index
if match := SCHEMA_PLUGIN_RE.match(basename):
return 1, schema_key(match.group(1)), basename, index
if match := MAPPING_PLUGIN_RE.match(basename):
return 2, schema_key(match.group(1)), basename, index
if match := DOCUMENT_PLUGIN_RE.match(basename):
return 3, schema_key(match.group(1)), basename, index
if match := GEOMETRY_SERIALIZATION_PLUGIN_RE.match(basename):
return 4, schema_key(match.group(1)), basename, index
return 5, schema_key(""), basename, index
def ordered_infos(infos: list[zipfile.ZipInfo]) -> list[zipfile.ZipInfo]:
shared_infos = [(index, info) for index, info in enumerate(infos) if info.filename.endswith(".so")]
ordered_shared_infos = [
info for index, info in sorted(shared_infos, key=lambda item: shared_object_sort_key(item[1].filename, item[0]))
]
ordered_shared_iter = iter(ordered_shared_infos)
return [next(ordered_shared_iter) if info.filename.endswith(".so") else info for info in infos]
def zip_info_for_write(source: zipfile.ZipInfo) -> zipfile.ZipInfo:
info = zipfile.ZipInfo(source.filename)
info.date_time = source.date_time
info.compress_type = source.compress_type
info.comment = source.comment
info.create_system = source.create_system
info.external_attr = source.external_attr
info.extra = source.extra
return info
def shared_object_names(infos: list[zipfile.ZipInfo]) -> list[str]:
return [info.filename for info in infos if info.filename.endswith(".so")]
def rewrite_wheel(wheel: Path, ordered: list[zipfile.ZipInfo]) -> None:
fd, temp_name = tempfile.mkstemp(prefix=f".{wheel.name}.", suffix=".tmp", dir=wheel.parent)
os.close(fd)
temp_path = Path(temp_name)
try:
with zipfile.ZipFile(wheel) as zin, zipfile.ZipFile(
temp_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
) as zout:
for info in ordered:
zout.writestr(zip_info_for_write(info), zin.read(info))
os.replace(temp_path, wheel)
finally:
if temp_path.exists():
temp_path.unlink()
def order_wheel(wheel: Path, check: bool) -> bool:
wheel = wheel.resolve()
if wheel.suffix != ".whl":
raise ValueError(f"not a wheel: {wheel}")
with zipfile.ZipFile(wheel) as zf:
infos = zf.infolist()
ordered = ordered_infos(infos)
changed = shared_object_names(infos) != shared_object_names(ordered)
if check:
if changed:
print(f"{wheel}: shared object order needs updating")
return False
print(f"{wheel}: shared object order is already valid")
return True
if changed:
rewrite_wheel(wheel, ordered)
print(f"{wheel}: reordered shared objects")
else:
print(f"{wheel}: shared object order is already valid")
return True
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("wheel", type=Path, help="Wheel to rewrite in place")
parser.add_argument("--check", action="store_true", help="Only validate the current shared object order")
args = parser.parse_args()
return 0 if order_wheel(args.wheel, args.check) else 1
if __name__ == "__main__":
raise SystemExit(main())
+232
View File
@@ -0,0 +1,232 @@
#
# /// script
# # Latest Pyodide build env versions are listed here:
# # https://pyodide.github.io/pyodide/api/pyodide-cross-build-environments.json
# # https://github.com/pyodide/pyodide-build/blob/main/pyodide_build/xbuildenv_releases.py
# requires-python = "==3.13.2"
# dependencies = [
# "requests",
# "setuptools",
# ]
# ///
"""
Pack an IfcOpenShell WASM wheel using Pyodide build system.
Usage:
uv run make_wheel.py # Show this help
uv run make_wheel.py --build # Build wheel
uv run make_wheel.py --clean # Clean build artifacts and exit
"""
import argparse
import os
import re
import shutil
import subprocess
import time
import zipfile
from pathlib import Path
from urllib.parse import quote
import requests
# Get repo root (parent of this script's parent directory)
REPO_ROOT = Path(__file__).parent.parent
PYODIDE_DIR = REPO_ROOT / "pyodide"
BUILD_DIR = PYODIDE_DIR / "build"
# Hardcoded path (Windows packing workaround with --dev flag)
PYODIDE_BUILD = Path(r"L:\Projects\Github\pyodide-build")
# Wheel platform tag (from PYODIDE_EMSCRIPTEN_VERSION in pyodide-build/Makefile.envs)
WHEEL_PLATFORM_TAG = "emscripten_4_0_9_wasm32"
# Location where ifcopenshell will be extracted
IFCOPENSHELL_DIR = PYODIDE_DIR / "ifcopenshell"
class WheelBuilder:
@staticmethod
def extract_ifcopenshell_from_git(dst: Path) -> None:
"""Extract ifcopenshell directory from git repo into destination."""
Tools.rmrf(dst)
print(f"Extracting ifcopenshell from git to {dst}...")
# Use git ls-files piped to git checkout-index to avoid copying
# untracked or ignored files from the actual repo.
ls_proc = subprocess.Popen(
["git", "ls-files", "-z", "src/ifcopenshell-python/ifcopenshell"],
cwd=REPO_ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
checkout_proc = subprocess.Popen(
["git", "checkout-index", "-z", "--prefix", "pyodide/", "--stdin"],
cwd=REPO_ROOT,
stdin=ls_proc.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
assert ls_proc.stdout is not None
ls_proc.stdout.close()
checkout_proc.communicate()
if checkout_proc.returncode != 0:
assert checkout_proc.stderr is not None
raise RuntimeError(f"Failed to extract: {checkout_proc.stderr.decode()}")
# Move src/ifcopenshell-python/ifcopenshell to ifcopenshell.
temp_src = PYODIDE_DIR / "src" / "ifcopenshell-python" / "ifcopenshell"
shutil.move(temp_src, dst)
# Clean up temporary src directory.
Tools.rmrf(PYODIDE_DIR / "src")
print("✓ Extracted ifcopenshell from git")
@staticmethod
def get_wheel_url(makefile_path: Path) -> str:
"""Get S3 wheel URL based on BINARY_VERSION and BUILD_COMMIT from Makefile."""
def parse_makefile_vars() -> dict[str, str]:
content = makefile_path.read_text()
vars: dict[str, str] = {}
for match in re.finditer(r"^(BINARY_VERSION|BUILD_COMMIT):=(.+)$", content, re.MULTILINE):
vars[match.group(1)] = match.group(2).strip()
return vars
vars: dict[str, str] = parse_makefile_vars()
binary_version = vars["BINARY_VERSION"]
build_commit = vars["BUILD_COMMIT"]
filename = f"ifcopenshell-{binary_version}+{build_commit}-cp313-cp313-pyodide_2025_0_wasm32.whl"
encoded_filename = quote(filename, safe="")
return f"https://s3.amazonaws.com/ifcopenshell-builds/{encoded_filename}"
@staticmethod
def download_and_extract_so(url: str, build_dir: Path) -> tuple[Path, Path]:
"""Download wheel from URL and extract .so and .py files."""
py_wrapper_filename = "ifcopenshell_wrapper.py"
build_dir.mkdir(parents=True, exist_ok=True)
wheel_path = build_dir / url.rsplit("/", 1)[-1]
if wheel_path.exists():
print(f"Using cached wheel: {wheel_path}")
else:
print(f"Downloading {url}...")
response = requests.get(url)
response.raise_for_status()
wheel_path.write_bytes(response.content)
print("Extracting _ifcopenshell_wrapper files...")
with zipfile.ZipFile(wheel_path) as zf:
so_files = [f for f in zf.namelist() if f.endswith(".so")]
py_files = [f for f in zf.namelist() if f.endswith(py_wrapper_filename)]
assert so_files, "No .so file found in wheel"
assert py_files, f"No {py_wrapper_filename} file found in wheel"
so_file = so_files[0]
so_dst = build_dir / Path(so_file).name
so_dst.write_bytes(zf.read(so_file))
py_file = py_files[0]
py_dst = build_dir / Path(py_file).name
py_dst.write_bytes(zf.read(py_file))
return so_dst, py_dst
class Tools:
@staticmethod
def run(
cmd: list[str],
cwd: Path | None = None,
) -> None:
print(f"$ {' '.join(cmd)}")
subprocess.check_call(cmd, cwd=cwd)
@staticmethod
def create_symlink(dst: Path, src: Path) -> None:
Tools.rmrf(dst)
dst.symlink_to(src)
@staticmethod
def rmrf(path: Path) -> None:
if path.exists() or path.is_symlink():
if path.is_dir() and not path.is_symlink():
shutil.rmtree(path)
else:
path.unlink()
def clean() -> None:
"""Remove build artifacts."""
paths_to_remove = (
BUILD_DIR,
PYODIDE_DIR / ".pyodide_build",
PYODIDE_DIR / "dist",
PYODIDE_DIR / "ifcopenshell.egg-info",
PYODIDE_DIR / "src",
IFCOPENSHELL_DIR,
)
for path in paths_to_remove:
if path.exists() or path.is_symlink():
print(f"Removing {path}...")
Tools.rmrf(path)
print("✓ Clean complete")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, add_help=False)
parser.add_argument("--build", action="store_true", help="Build the wheel")
parser.add_argument("--clean", action="store_true", help="Clean build folder")
parser.add_argument(
"--dev",
action="store_true",
help="Use editable pyodide-build from hardcoded path (Windows packing workaround)",
)
args = parser.parse_args()
if not args.build and not args.clean:
print(__doc__)
return
if args.clean:
clean()
return
start_time = time.time()
WheelBuilder.extract_ifcopenshell_from_git(IFCOPENSHELL_DIR)
print("Downloading and extracting _ifcopenshell_wrapper files...")
makefile = REPO_ROOT / "src" / "ifcopenshell-python" / "Makefile"
wheel_url = WheelBuilder.get_wheel_url(makefile)
so_file, py_file = WheelBuilder.download_and_extract_so(wheel_url, BUILD_DIR)
Tools.create_symlink(IFCOPENSHELL_DIR / Path(so_file).name, so_file)
Tools.create_symlink(IFCOPENSHELL_DIR / Path(py_file).name, py_file)
print("Installing pyodide-build...")
if args.dev:
Tools.run(["uv", "pip", "install", "-e", str(PYODIDE_BUILD)])
else:
Tools.run(["uv", "pip", "install", "pyodide-build"])
print("Building with pyodide...")
# Use --no-isolation due to pyodide-build Windows support issues:
# symlink_unisolated_packages fails with missing `_sysconfigdata_$(CPYTHON_ABI_FLAGS)_emscripten_wasm32-emscripten.py`.
# Hardcode platform name since pyodide doesn't yet support overriding wheel tags on Windows.
#
# Use `LEGACY_PLATFORM` since pyodide 0.34.1 introduced new tag for wheels `pyemscripten`,
# which doesn't work with pyodide itself yet - https://github.com/pyodide/pyodide/issues/6177.
os.environ["USE_LEGACY_PLATFORM"] = "1"
Tools.run(["pyodide", "build", f"-C--build-option=--plat-name={WHEEL_PLATFORM_TAG}"])
elapsed = time.time() - start_time
print(f"\n✓ Done! ({elapsed:.1f}s)")
if __name__ == "__main__":
main()
+39 -1
View File
@@ -2,12 +2,16 @@
# because `tool.setuptools.ext-modules` is still experimental in pyproject.toml
# and we need it to get the wheel suffix right.
import os
import sys
from pathlib import Path
import tomllib
from setuptools import Extension, find_packages, setup
from setuptools.command.build_ext import build_ext
REPO_FOLDER = Path(__file__).parent
# Detect repo folder: if setup.py is in pyodide folder, go to parent
SETUP_DIR = Path(__file__).parent
REPO_FOLDER = SETUP_DIR.parent if SETUP_DIR.name == "pyodide" else SETUP_DIR
def get_version() -> str:
@@ -25,6 +29,39 @@ def get_dependencies() -> list[str]:
return dependencies
class UnixBuildExt(build_ext):
"""Customize ``build_ext`` to support packing on Windows."""
def finalize_options(self):
from distutils import sysconfig
super().finalize_options()
if sys.platform == "win32":
self.compiler = "unix"
# Configure sysconfig for Windows builds
# CCSHARED is the only variable that's not customizable with env vars.
# Basically avoiding this:
# File ".venv\Lib\site-packages\setuptools\_distutils\sysconfig.py", line 366, in customize_compiler
# compiler_so=cc_cmd + ' ' + ccshared,
# ~~~~~~~~~~~~~^~~~~~~~~~
# TypeError: can only concatenate str (not "NoneType") to str
sysconfig.get_config_vars() # Initialize config cache
if sysconfig._config_vars.get("CCSHARED") is None:
sysconfig._config_vars["CCSHARED"] = "-fPIC"
# Override compiler type before it's instantiated
# Set Emscripten compiler environment variables
os.environ["CC"] = "emcc"
os.environ["CXX"] = "em++"
os.environ["CFLAGS"] = ""
os.environ["CXXFLAGS"] = ""
os.environ["LDSHARED"] = "emcc -shared"
os.environ["AR"] = "emar"
os.environ["ARFLAGS"] = "rcs"
os.environ["SETUPTOOLS_EXT_SUFFIX"] = ".cpython-313-wasm32-emscripten.so"
setup(
name="ifcopenshell",
version=get_version(),
@@ -44,4 +81,5 @@ setup(
},
# Has to provide extension to get the correct wheel suffix.
ext_modules=[Extension("ifcopenshell._ifcopenshell_wrapper", sources=[])],
cmdclass={"build_ext": UnixBuildExt},
)
+296
View File
@@ -0,0 +1,296 @@
#!/usr/bin/env python3
"""Split optional IfcOpenShell Pyodide payloads into separate wheels."""
from __future__ import annotations
import argparse
import base64
import csv
import hashlib
import io
import os
import re
import sys
import time
import zipfile
from email.parser import Parser
from pathlib import Path
MAIN_SHARED_OBJECT_RE = re.compile(r"(^|/)_ifcopenshell_wrapper(?:\.|$)")
PURE_PYTHON_PACKAGE_NAME = "ifcopenshell-pure-python"
PURE_PYTHON_PREFIXES = (
"ifcopenshell/api/",
"ifcopenshell/express/",
"ifcopenshell/mvd/",
"ifcopenshell/simple_spf/",
)
def wheel_parts(path: Path) -> tuple[str, str, str, str, str]:
if path.suffix != ".whl":
raise ValueError(f"not a wheel: {path}")
stem = path.name[:-4]
left, py_tag, abi_tag, platform_tag = stem.rsplit("-", 3)
dist, version = left.rsplit("-", 1)
return dist, version, py_tag, abi_tag, platform_tag
def safe_name(name: str) -> str:
return re.sub(r"[-_.]+", "-", name).lower().strip("-")
def wheel_escape(value: str) -> str:
return re.sub(r"[^\w\d.]+", "_", value, flags=re.UNICODE)
def wheel_version_escape(value: str) -> str:
return re.sub(r"[^\w\d.+]+", "_", value, flags=re.UNICODE)
def dist_info_dir(name: str, version: str) -> str:
return f"{wheel_escape(name)}-{wheel_version_escape(version)}.dist-info"
def sha256_record_value(data: bytes) -> str:
digest = hashlib.sha256(data).digest()
return "sha256=" + base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=")
def make_info(name: str, *, source: zipfile.ZipInfo | None = None, mode: int | None = None) -> zipfile.ZipInfo:
info = zipfile.ZipInfo(name)
if source is not None:
info.date_time = source.date_time
info.external_attr = source.external_attr
info.comment = source.comment
info.create_system = source.create_system
else:
info.date_time = time.localtime(time.time())[:6]
info.external_attr = ((mode if mode is not None else 0o644) & 0xFFFF) << 16
info.create_system = 3
info.compress_type = zipfile.ZIP_DEFLATED
return info
def write_record(zf: zipfile.ZipFile, entries: dict[str, bytes | None], record_name: str) -> None:
rows: list[list[str]] = []
for name in sorted(entries):
data = entries[name]
if name == record_name:
rows.append([name, "", ""])
elif data is None:
raise ValueError(f"missing bytes for RECORD entry {name}")
else:
rows.append([name, sha256_record_value(data), str(len(data))])
buf = io.StringIO(newline="")
writer = csv.writer(buf, lineterminator="\n")
writer.writerows(rows)
zf.writestr(make_info(record_name), buf.getvalue().encode("utf-8"))
def read_original_metadata(zf: zipfile.ZipFile) -> tuple[str, str, str]:
metadata_names = [n for n in zf.namelist() if n.endswith(".dist-info/METADATA")]
wheel_names = [n for n in zf.namelist() if n.endswith(".dist-info/WHEEL")]
record_names = [n for n in zf.namelist() if n.endswith(".dist-info/RECORD")]
if len(metadata_names) != 1 or len(wheel_names) != 1 or len(record_names) != 1:
raise ValueError("expected exactly one METADATA, WHEEL, and RECORD in the source wheel")
return metadata_names[0], wheel_names[0], record_names[0]
def shared_package_name(so_path: str) -> str:
stem = Path(so_path).name.removesuffix(".so")
stem = re.sub(r"[^A-Za-z0-9]+", "-", stem).strip("-")
return safe_name(stem)
def is_pure_python_split_path(path: str) -> bool:
return any(path.startswith(prefix) for prefix in PURE_PYTHON_PREFIXES)
def build_wheel(
output_dir: Path,
package_name: str,
version: str,
tag: str,
root_is_purelib: bool,
summary: str,
payloads: list[tuple[zipfile.ZipInfo, bytes]],
license_files: dict[str, bytes],
) -> Path:
di = dist_info_dir(package_name, version)
wheel_name = f"{wheel_escape(package_name)}-{wheel_version_escape(version)}-{tag}.whl"
out = output_dir / wheel_name
record_name = f"{di}/RECORD"
entries: dict[str, bytes | None] = {}
metadata = (
"Metadata-Version: 2.4\n"
f"Name: {package_name}\n"
f"Version: {version}\n"
f"Summary: {summary}\n"
"License-File: COPYING\n"
"License-File: COPYING.LESSER\n"
"\n"
).encode()
wheel = (
"Wheel-Version: 1.0\n"
"Generator: split_pyodide_ifcopenshell_wheel.py\n"
f"Root-Is-Purelib: {str(root_is_purelib).lower()}\n"
f"Tag: {tag}\n"
"\n"
).encode()
with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
for info, data in payloads:
zf.writestr(make_info(info.filename, source=info), data)
entries[info.filename] = data
metadata_name = f"{di}/METADATA"
wheel_meta_name = f"{di}/WHEEL"
zf.writestr(make_info(metadata_name), metadata)
zf.writestr(make_info(wheel_meta_name), wheel)
entries[metadata_name] = metadata
entries[wheel_meta_name] = wheel
for basename, data in license_files.items():
name = f"{di}/licenses/{basename}"
zf.writestr(make_info(name), data)
entries[name] = data
entries[record_name] = None
write_record(zf, entries, record_name)
return out
def rewrite_main_wheel(source: Path, target: Path, split_paths: set[str]) -> None:
with zipfile.ZipFile(source) as zin, zipfile.ZipFile(
target, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
) as zout:
_, _, record_name = read_original_metadata(zin)
entries: dict[str, bytes | None] = {}
for info in zin.infolist():
if info.filename in split_paths or info.filename == record_name:
continue
data = zin.read(info.filename)
zout.writestr(make_info(info.filename, source=info), data)
entries[info.filename] = data
entries[record_name] = None
write_record(zout, entries, record_name)
def verify_wheel(path: Path) -> None:
with zipfile.ZipFile(path) as zf:
zf.testzip()
metadata_name, wheel_name, record_name = read_original_metadata(zf)
Parser().parsestr(zf.read(metadata_name).decode("utf-8"))
wheel_text = zf.read(wheel_name).decode("utf-8")
if "Wheel-Version:" not in wheel_text or "Tag:" not in wheel_text:
raise ValueError(f"invalid WHEEL metadata in {path}")
record_rows = list(csv.reader(io.StringIO(zf.read(record_name).decode("utf-8"))))
names = {row[0] for row in record_rows}
missing = set(zf.namelist()) - names
if missing:
raise ValueError(f"{path} RECORD is missing entries: {sorted(missing)[:5]}")
for name, digest, size in record_rows:
if name == record_name:
continue
data = zf.read(name)
if digest != sha256_record_value(data) or size != str(len(data)):
raise ValueError(f"{path} RECORD mismatch for {name}")
def split_wheel(wheel_path: Path, output_dir: Path) -> None:
wheel_path = wheel_path.expanduser().resolve()
if not wheel_path.exists():
raise FileNotFoundError(wheel_path)
output_dir = output_dir.expanduser().resolve()
output_dir.mkdir(parents=True, exist_ok=True)
main_wheel_path = output_dir / wheel_path.name
if main_wheel_path.resolve(strict=False) == wheel_path:
raise ValueError("output directory must not point to the input wheel location")
_, version, py_tag, abi_tag, platform_tag = wheel_parts(wheel_path)
binary_tag = f"{py_tag}-{abi_tag}-{platform_tag}"
pure_tag = "py3-none-any"
with zipfile.ZipFile(wheel_path) as zf:
file_infos = [info for info in zf.infolist() if not info.is_dir()]
so_infos = [info for info in file_infos if info.filename.endswith(".so")]
split_so_infos = [info for info in so_infos if not MAIN_SHARED_OBJECT_RE.search(Path(info.filename).name)]
pure_python_infos = [info for info in file_infos if is_pure_python_split_path(info.filename)]
if not split_so_infos and not pure_python_infos:
raise RuntimeError("no secondary .so files or pure Python subpackages found to split")
license_files = {
Path(info.filename).name: zf.read(info.filename)
for info in file_infos
if ".dist-info/licenses/" in info.filename
}
split_so_payloads = [(info, zf.read(info.filename)) for info in split_so_infos]
pure_python_payloads = [(info, zf.read(info.filename)) for info in pure_python_infos]
created_wheels: list[Path] = []
for info, data in split_so_payloads:
package_name = shared_package_name(info.filename)
created_wheels.append(
build_wheel(
output_dir,
package_name,
version,
binary_tag,
False,
f"Pyodide shared library split from IfcOpenShell ({Path(info.filename).name}).",
[(info, data)],
license_files,
)
)
if pure_python_payloads:
created_wheels.append(
build_wheel(
output_dir,
PURE_PYTHON_PACKAGE_NAME,
version,
pure_tag,
True,
"Pure Python subpackages split from IfcOpenShell.",
pure_python_payloads,
license_files,
)
)
temp_main_wheel = output_dir / f".{wheel_path.name}.tmp"
try:
rewrite_main_wheel(
wheel_path,
temp_main_wheel,
{info.filename for info, _ in split_so_payloads + pure_python_payloads},
)
verify_wheel(temp_main_wheel)
for created in created_wheels:
verify_wheel(created)
os.replace(temp_main_wheel, main_wheel_path)
finally:
if temp_main_wheel.exists():
temp_main_wheel.unlink()
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Extract optional IfcOpenShell Pyodide payloads into separate wheel artifacts."
)
parser.add_argument("wheel", help="IfcOpenShell Pyodide wheel to split")
parser.add_argument("output_dir", help="Directory for generated wheels")
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(sys.argv[1:] if argv is None else argv)
split_wheel(Path(args.wheel), Path(args.output_dir))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+2
View File
@@ -14,6 +14,8 @@ def test_ifcopenshell_import(selenium):
import micropip
await micropip.install(f"./{WHEEL_FILENAME}")
import ifcopenshell
from pathlib import Path
ifcopenshell.set_plugin_search_paths([str(Path(ifcopenshell.__file__).parent)])
ifc_file = ifcopenshell.file()
wall = ifc_file.create_entity("IfcWall")
wall1 = ifc_file.by_type("IfcWall")[0]
+144 -12
View File
@@ -1,12 +1,8 @@
[project]
name = "IfcOpenShell"
version = "0.0.0"
dependencies = [
"black==26.1.0",
"ruff==0.15.0",
"poethepoet",
"gersemi==0.25.4",
]
# Don't provide requires-python explicitly
# allowing pyprojects to set their own (e.g. bonsai and general ifcopenshell version differ).
[tool.black]
line-length = 120
@@ -15,7 +11,8 @@ include = '''
|nix/.*.pyi?$
'''
extend-exclude = '''
src/ifcopenshell-python/ifcopenshell/express/*
src/ifcopenshell-python/ifcopenshell/express/rules/*
|src/ifcopenshell-python/ifcopenshell/express/express_parser.py
|src/ifcopenshell-python/ifcopenshell/mvd/*
|src/ifcopenshell-python/ifcopenshell/simple_spf/*
|src/ifc2ca/templates/*
@@ -27,11 +24,21 @@ extend-exclude = '''
reportInvalidTypeForm = false
disableBytesTypePromotions = true
reportUnnecessaryTypeIgnoreComment = true
reportRedeclaration = false
# Ignore warnings from bpy stubs missing actual source files.
reportMissingModuleSource = false
# Pylance doesn't respect gitignore, so we have to exclude files manually here
# to avoid VS Code slowing down.
# https://github.com/microsoft/pylance-release/issues/5169
exclude = [
"_deps",
]
# Define here general ruff settings,
# then they will be inherited by projects' .toml files.
# This allows using assuming different Python version for different projects.
[tool.ruff]
line-length = 120
exclude = [
# Submodules.
"src/ifcopenshell-python/ifcopenshell/express",
@@ -71,15 +78,140 @@ ignore = [
"UP032", # Replace .format with f-string
]
[tool.ty.rules]
all = "error"
# Structural rules (no deep type inference needed, easier to adapt).
# Maybe later, requires to specify element types for all generics.
missing-type-argument = "ignore"
# Conflicts with `bpy` props defined using annotations.
invalid-type-form = "ignore"
# Non-structural rules:
# Conflicts with `ifcopenshell.api.geometry.add_representation` type of callables we have, confusing them with a module.
call-non-callable = "ignore"
# bpy is missing some context manager implementations.
invalid-context-manager = "ignore"
# Doesn't go well with `bpy.ops.xxx.yyy`.
unresolved-attribute = "ignore"
# Too many false positives.
invalid-argument-type = "ignore"
invalid-method-override = "ignore"
invalid-assignment = "ignore"
invalid-parameter-default = "ignore"
missing-override-decorator = "ignore"
invalid-yield = "ignore"
invalid-return-type = "ignore"
non-callable-init-subclass = "ignore"
not-iterable = "ignore"
possibly-missing-attribute = "ignore"
no-matching-overload = "ignore"
not-subscriptable = "ignore"
unsupported-dynamic-base = "ignore"
unsupported-operator = "ignore"
[tool.ty.environment]
extra-paths = [
"src/bonsai/external_dependencies",
"src/bcf",
"src/bsdd",
"src/bonsai",
"src/ifc4d",
"src/ifc5d",
"src/ifccityjson",
"src/ifcclash",
"src/ifccsv",
"src/ifcdiff",
"src/ifcfm",
"src/ifcopenshell-python",
"src/ifcpatch",
"src/ifctester",
]
[tool.ty.src]
exclude = [
# External dependencies cloned for type checking only.
"src/bonsai/external_dependencies",
# Submodules.
"src/ifcopenshell-python/ifcopenshell/express",
"src/ifcopenshell-python/ifcopenshell/mvd",
"src/ifcopenshell-python/ifcopenshell/simple_spf",
"src/svgfill/3rdparty",
# Has special dependencies.
"src/ifcopenshell-python/ifcopenshell/geom/app.py",
"src/ifcopenshell-python/ifcopenshell/geom/code_editor_pane.py",
"src/ifcopenshell-python/ifcopenshell/util/doc.py",
"src/ifcopenshell-python/ifcopenshell/util/generate_pset_templates.py",
"src/ifcopenshell-python/ifcopenshell/util/ifc4x3dev_scrape_data_for_docs.py",
# Too esoteric.
"src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py",
"src/ifc2ca/templates",
# Too dev.
"src/bcf/setup.py",
"src/bsdd/yml_to_classes.py",
# Deprecated.
"src/ifc2ca/_deprecated",
]
[tool.poe.tasks]
ruff-main = "ruff check --extend-exclude nix/build-all.py"
# It's actually Python 3.6, but ruff only supports 3.7+, but it should do.
ruff-old = "ruff check nix/build-all.py --target-version py37"
ruff.sequence = ["ruff-main", "ruff-old"]
dev-setup.sequence = [
# 3.13 is chosen because it's the version used in the latest Bonsai.
{cmd = "uv sync --python 3.13"},
{cmd = "uv pip install -e ./src/bsdd/"},
{cmd = "uv pip install -e ./src/ifcopenshell-python/[advanced,dev]"},
{cmd = "uv pip install -e ./src/ifcedit/"},
{cmd = "uv pip install -e ./src/ifcpatch/"},
{cmd = "uv pip install -e ./src/ifcquery/"},
{cmd = "uv pip install -e './src/ifcmcp/[mcp]'"},
{cmd = "uv pip install -r src/bonsai/requirements-dev.txt"},
]
dev-setup.help = "Install repo packages in editable mode"
ruff = "ruff check"
black = "black ."
format.sequence = ["black", "ruff-main", "ruff-old"]
ty.sequence = ["ty-bonsai", "ty-ios"]
ty.help = "Run ty type checker. Requires ty-venv to be set up first."
ty-bonsai = "ty check src/bonsai --python=src/bonsai/.venv"
ty-venv.sequence = ["bonsai-deps", "ty-venv-bonsai", "ty-venv-ios"]
ty-venv-bonsai.sequence = [
{cmd = "uv venv src/bonsai/.venv --python=3.11 --allow-existing"},
{cmd = "uv pip install -r src/bonsai/type-check-requirements.txt --python=src/bonsai/.venv"},
]
ty-venv-ios.sequence = [
{cmd = "uv venv src/ifcopenshell-python/.venv --python=3.10 --allow-existing"},
{cmd = "uv pip install -r src/ifcopenshell-python/type-check-requirements.txt --python=src/ifcopenshell-python/.venv"},
]
format.sequence = ["black", "ruff"]
cmake-format = "gersemi . --in-place"
[tool.poe.tasks.ty-ios]
cmd = """
ty check
nix/
src/bcf
src/bsdd
src/ifc2ca
src/ifc4d
src/ifc5d
src/ifccityjson
src/ifcclash
src/ifccsv
src/ifcdiff
src/ifcfm
src/ifcopenshell-python
src/ifcpatch
src/ifctester
--python=src/ifcopenshell-python/.venv
"""
[tool.poe.tasks.bonsai-deps]
help = "Clone or update Bonsai external dependencies."
cmd = "python src/bonsai/scripts/bonsai_deps.py"
+5
View File
@@ -0,0 +1,5 @@
black==26.3.1
ruff==0.16.0
poethepoet
ty==0.0.63
gersemi==0.28.0
+1 -1
View File
@@ -316,7 +316,7 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
Returns:
The BCF viewpoint definition.
"""
ifc_file = element.wrapped_data.file
ifc_file = element.file
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
elem_placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
elem_placement[:3, 3] *= unit_scale
+9 -3
View File
@@ -34,8 +34,8 @@ client_id, client_secret = "", ""
class OAuthReceiver(http.server.BaseHTTPRequestHandler):
def do_GET(self) -> None:
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
self.server.auth_code = query.get("code", [""])[0] # type:ignore
self.server.auth_state = query.get("state", [""])[0] # type:ignore
self.server.auth_code = query.get("code", [""])[0]
self.server.auth_state = query.get("state", [""])[0]
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
@@ -188,6 +188,8 @@ class BcfClient:
response.raise_for_status()
return response.status_code, response.text
except requests.exceptions.HTTPError as errh:
response = errh.response
assert response is not None
print(f"message: {response.reason}' '{response.status_code}, {errh}")
return response.status_code, response.reason
@@ -206,6 +208,8 @@ class BcfClient:
response.raise_for_status()
return response.status_code, response.text
except requests.exceptions.HTTPError as errh:
response = errh.response
assert response is not None
print(f"message: {response.reason}' '{response.status_code}, {errh}")
return response.status_code, response.reason
@@ -222,6 +226,8 @@ class BcfClient:
response.raise_for_status()
return response.status_code, response.text
except requests.exceptions.HTTPError as errh:
response = errh.response
assert response is not None
print(f"message: {response.reason}' '{response.status_code}, {errh}")
return response.status_code, response.reason
@@ -255,7 +261,7 @@ class BcfClient:
project_id: str = "",
topics: str = "",
query_string: Optional[str] = None,
) -> list[Any]:
) -> None:
# return self.get(
# f"/projects/{project_id}/topics",
# {
+1 -1
View File
@@ -316,7 +316,7 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
Returns:
The BCF viewpoint definition.
"""
ifc_file = element.wrapped_data.file
ifc_file = element.file
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
elem_placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
elem_placement[:3, 3] *= unit_scale
+14 -10
View File
@@ -173,16 +173,17 @@ def assert_viewpoints(viewpoints):
assert viewpoint.snapshot is not None
# TODO: dead code - ported from v2 but buildingSMART/BCF-XML has no v3 MaximumInformation.bcf equivalent
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
expected_vp = mdl.VisualizationInfo(
components=mdl.Components(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=False,
),
selection=expected_selection,
visibility=mdl.ComponentVisibility(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=False,
),
exceptions=expected_exception,
default_visibility=False,
),
@@ -193,6 +194,7 @@ def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, e
camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266),
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
field_of_view=60,
aspect_ratio=1.0,
),
guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
)
@@ -200,16 +202,17 @@ def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, e
assert viewpoint.snapshot is not None
# TODO: dead code - ported from v2 but buildingSMART/BCF-XML has no v3 MaximumInformation.bcf equivalent
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
expected_vp = mdl.VisualizationInfo(
components=mdl.Components(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=True,
),
selection=expected_selection,
visibility=mdl.ComponentVisibility(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=True,
),
exceptions=expected_exception,
default_visibility=True,
),
@@ -220,6 +223,7 @@ def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, ex
camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428),
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
field_of_view=60,
aspect_ratio=1.0,
),
guid="81daa431-bf01-4a49-80a2-1ab07c177717",
)
+21 -31
View File
@@ -17,8 +17,8 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
SHELL := sh
PYTHON:=python3.11
PIP:=pip3.11
PYTHON:=python3
PIP:=pip3
PATCH:=patch
SED:=sed -i
VENV_ACTIVATE:=bin/activate
@@ -48,6 +48,7 @@ VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
VERSION_DATE:=$(shell date '+%y%m%d')
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
LAST_GIT_BRANCH:=$(shell git rev-parse --abbrev-ref HEAD)
PYPI_IMP:=cp
ifdef PYVERSION
@@ -63,6 +64,7 @@ PYNUMBER:=3$(PYMINOR)
PYPI_VERSION:=3.$(PYMINOR)
endif # def PYVERSION
IFCMERGE_VERSION:=2026-04-07
ifdef PLATFORM
SUPPORTED_PLATFORMS := linux macos macosm1 win
@@ -104,7 +106,7 @@ endif
endif # def PLATFORM
# Current build commit hash.
OLD:=e8eb5e4
OLD:=3e7b739
.PHONY: bump
bump:
ifndef NEW
@@ -190,7 +192,11 @@ endif
# Provides networkx graph analysis for project dependency calculations
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download networkx --dest=./wheels
# Required by IFCDiff
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download deepdiff --dest=./wheels
# Pinned <9.1: deepdiff 9.1.0 adds cachebox<6,>=5.2 which only ships macOS x86_64
# wheels for macosx_10_12+ and is incompatible with our macos py311 --platform
# macosx_10_10_x86_64 target. Revisit once the macos py311 platform tag is bumped
# to 10_13 (matching py312/py313).
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download "deepdiff<9.1" --dest=./wheels
# Required by IFCCSV and ifcopenshell.util.selector
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download lark --dest=./wheels
# Required by IFC4D
@@ -223,19 +229,8 @@ endif
cd build/bonsai/bim/data/gantt/ && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css
# Provides IFCJSON functionality
cd build && wget -O ifc2json.zip https://github.com/IFCJSON-Team/IFC2JSON_python/archive/refs/heads/master.zip
cd build && unzip ifc2json.zip && rm ifc2json.zip
# IFCJSON doesn't have pyproject.toml, so we use python command.
cd build && . env/$(VENV_ACTIVATE) && cd IFC2JSON_python-*/file_converters && \
$(PYTHON) -c "from setuptools import setup; \
setup( \
name='ifcjson', \
version='0.0.1', \
author='Jan Brouwer', \
author_email='jan@brewsky.nl', \
packages=['ifcjson'], \
)" bdist_wheel
cp -r build/IFC2JSON_python-*/file_converters/dist/*.whl build/wheels/
# TODO: Use official repo, once https://github.com/IFCJSON-Team/IFC2JSON_python/pull/8 is merged.
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/Andrej730/IFC2JSON_python.git@pyproject_toml" --no-deps -w wheels/
# Brickschema requires pkg_resources which is provided by Blender.
# Provides Brickschema functionality
@@ -243,26 +238,16 @@ endif
cd build/bonsai/bim/data/brick/ && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
# Required for hipped roof generation
cd build && wget https://github.com/prochitecture/bpypolyskel/archive/refs/heads/master.zip
cd build && unzip master.zip && rm master.zip
cd build && . env/$(VENV_ACTIVATE) && cd bpypolyskel-master && \
$(PYTHON) -c "from setuptools import setup; \
setup( \
name='bpypolyskel', \
version='0.0.0', \
packages=['bpypolyskel'], \
)" bdist_wheel
cp -r build/bpypolyskel-master/dist/*.whl build/wheels/
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/prochitecture/bpypolyskel" --no-deps -w wheels/
# folder for executable files
mkdir -p build/bonsai/libs/bin
# required for three-way git merging
ifeq ($(PLATFORM), win)
cd build/bonsai/libs/bin && wget https://github.com/brunopostle/ifcmerge/releases/download/2025-01-26/ifcmerge.zip
cd build/bonsai/libs/bin && unzip ifcmerge.zip && rm ifcmerge.zip
cd build/bonsai/libs/bin && wget https://github.com/brunopostle/ifcmerge/releases/download/$(IFCMERGE_VERSION)/ifcmerge.exe
else
cd build/bonsai/libs/bin && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/main/ifcmerge && chmod +x ifcmerge
cd build/bonsai/libs/bin && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/$(IFCMERGE_VERSION)/ifcmerge && chmod +x ifcmerge
endif
# Generate translations module for Bonsai build
@@ -281,6 +266,7 @@ else
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
$(SED) "s/7777777/$(LAST_GIT_BRANCH)/" build/bonsai/__init__.py
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
endif
@@ -371,9 +357,13 @@ test-tool:
ifndef MODULE
pytest test/tool
else
pytest test/tool/test_$(MODULE).py
pytest test/tool/test_$(MODULE).py --maxfail=1
endif
.PHONY: test-modal
test-modal:
blender --enable-event-simulate --python test/modal/test_modal.py --window-maximized
# Reregistering test is not added to the standard test suite because during unregister
# Blender removes all Bonsai dependencies breaking dev-environment symlinks.
.PHONY: test-reregister

Some files were not shown because too many files have changed in this diff Show More