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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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>
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>
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.
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>
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>
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>
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.