Implement get_vertical_bounding_planes using ray-casting from the RL
cut elevation with nearest-hit and coplanar grouping.
Generated with the assistance of an AI coding tool.
Design for extending generate_space with a hybrid parametric
extrusion + clipping / B-rep fallback strategy supporting sloped
roofs, sloped walls, sloped slabs, and curved walls.
Generated with the assistance of an AI coding tool.
Verifies that convert_curve_to_mesh produces the closing edge instead of overwriting the last segment, matching the fix from PR #8043.
Generated with the assistance of an AI coding tool.
convert_curve_to_mesh built the edge chain of a polyline with extend, then for a
closed polyline overwrote the last edge with the closing edge instead of
appending it. That discarded the final real segment, so every closed IfcPolyline
loop came back one edge short and open. On the edit mode round trip the inner
void loop of an IfcArbitraryProfileDefWithVoids was then lost or misclassified,
and the profile was rewritten without its void, collapsing the extrusion to a
bounding box.
Append the closing edge instead, matching the IfcIndexedPolyCurve branch. Live
tested: the Tab round trip now keeps both loops closed and re-exports the
IfcArbitraryProfileDefWithVoids with its inner void intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_face_matrix_from_verts used only the first 3 vertices and sb.np_normal, which divides by zero when they are collinear. Triangulated meshes from generated spaces often start with collinear boundary vertices, producing NaN matrices and a shapely LinearRing error. Walk the polygon to find a non-degenerate normal and edge.
Generated with the assistance of an AI coding tool.
The auto-generate-ifc-based-space-boundaries path builds a geometry cache from the IFC file. If a user (or a BDD helper) only moves the Blender object matrix, the cache still sees the old IFC placement and the space footprint is open. Commit any moved visible bounding objects and clear the cache before generating the space.
Generated with the assistance of an AI coding tool.
When regenerating an existing IfcSpace, the default container is no longer required. Instead, the space's container is found via get_parent(element), which walks the full spatial hierarchy (aggregation, containment, nesting). For new space creation, the default container is still required.
Add optional container parameter to get_space_polygon_from_context_visible_objects so regeneration can pass the resolved container directly.
Generated with the assistance of an AI coding tool.
Vectorize the coplanarity prefilter in _union_coplanar_face_polygon and
compute per-triangle normals once instead of per space face. Add
regression tests for the SmallHouse and Triangle boundary test models.
Generated with the assistance of an AI coding tool.
dissolve_faces with merge_coplanar drops interior rings, so the shaft
opening in a ceiling was lost and replaced by spurious wall-cap
boundaries. Reconstruct the space face from its raw coplanar triangles,
preserve interior rings in the assigned boundary, absorb redundant
candidates by plane offset, and raise the full-face tolerance so walls
offset by their half thickness get a single boundary.
Generated with the assistance of an AI coding tool.
When several elements match the same space face, offset matches that only
duplicate coplanar coverage are now skipped, and a single bounding element
within a small plane offset gets the full space face instead of a clipped
polygon. Existing boundaries are removed before regeneration so stale 2nd
level boundaries are not left behind, and the Bonsai operator delegates
element filtering to auto_generate_boundaries.
Regenerates SmallHouse boundaries to match the reference output and keeps
the ExternalEarth opening unioning intact.
Generated with the assistance of an AI coding tool.
When a building element has multiple ngons matching the same space face,
_process_openings was called multiple times for the same opening/filling,
producing duplicate boundaries (e.g. two boundaries for the same door).
Fix: pass a set of processed filling IDs to _process_openings and skip
already-processed openings.
Generated with the assistance of an AI coding tool.
When merge_coplanar merges two sub-faces that share an edge from the
original BRep (e.g. two rectangles forming an L-shape cap), that shared
edge remained in boundary_edges via original_edges filtering, causing
the edge_adjacency walk to produce wrong polygons.
Fix: after coplanar merging, use edge frequency (edges used by exactly
1 triangle = boundary) instead of original_edges filtering, which
correctly identifies only outer boundary edges.
Also add safety checks: edge_adjacency emptiness guard, infinite loop
protection, and minimum polygon length check.
Generated with the assistance of an AI coding tool.
The a2p placement matrix stores col[0]=X (edge direction) and
col[2]=Z (face normal), but assign_connection_geometry expects
axis=Z (normal) and ref_direction=X (edge).
Generated with the assistance of an AI coding tool.
Move Blender-independent boundary generation algorithm from Bonsai
(GPL) to ifcopenshell.util.boundary (LGPL):
- ifcopenshell.util.shape.dissolve_faces: reconstruct polygonal faces
from triangulated mesh using original edges from get_edges() + Union-Find
- ifcopenshell.util.boundary.auto_generate_boundaries: full boundary
generation algorithm using IFC geometry (numpy, shapely) without
Blender — replaces bmesh, matrix_world, tool.Cad.is_x, mathutils with
numpy equivalents
- Uses existing ifcopenshell.api.boundary.assign_connection_geometry
for connection geometry creation
- Uses existing ifcopenshell.util.placement.a2p + np_normal for face
matrix construction
- BOUNDARY_ELEMENT_CLASSES expanded to include IfcColumn and
IfcCurtainWall
Bonsai's boundary/operator.py auto_generate_boundaries is now a thin
adapter handling Blender-specific preprocessing (flushing moved
objects, building iterator + tree) then delegating to the util module.
Added 12 tests: 3 for dissolve_faces, 3 for auto_generate_boundaries.
Generated with the assistance of an AI coding tool.
Add buffer(0) validation for space_face_polygon and face_polygon
before intersection, following the same pattern as tool/cad.py.
Wrap the intersection in try/except for shapely.errors.GEOSException
to catch remaining topology errors. On exception, set
bonsai.last_error (so the 'Copy Error Message To Clipboard' button
appears in the UI), report an ERROR to the operator, and continue
processing other face pairs instead of crashing.
Generated with the assistance of an AI coding tool.
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.
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.