get_vertical_bounding_planes now also casts rays from the footprint
polygon's corner vertices, offset slightly inward toward the centroid.
This catches bounding elements (e.g. sloped roofs) that only cover a
corner of the space.
Generated with the assistance of an AI coding tool.
Fix three issues in generate_space:
1. Z location drift: z was derived from the Blender bounding box, which
changes after every regeneration. Use active_obj.location.z instead.
2. Cache invalidation for moved roofs/slabs: commit placements for
HEIGHT_DETECTION_CLASSES in addition to BOUNDING_CLASSES so the
geometry cache reflects recent moves.
3. Non-deterministic regeneration: the old Body representation was still
present in the IFC file when get_space_volume_strategy built the
geometry tree, so ray hits from get_vertical_bounding_planes hit the
space's own body. Since each regeneration produced a different Body
(BooleanClippingResult/FacetedBrep), the strategy alternated between
EXTRUDE_CLIP and BREP. Remove all Body representations before
strategy detection so the tree only contains bounding elements.
Also clean up stale IfcRelSpaceBoundary relationships before each
regeneration to prevent old boundary references from contaminating
subsequent runs. Remove ALL existing Body representations (not just
the first one found) to prevent duplicate half-space clipping chains.
Add regression tests including a 5-iteration stability check.
Generated with the assistance of an AI coding tool.
set_space_representation_from_polygon was localising the footprint and
clipping planes by subtracting only the object origin. For spaces with a
rotated ObjectPlacement (e.g. Space 5710 in the test IFC) the footprint
was not rotated into the space's local coordinate system, so the
regenerated mesh was rotated by the placement angle and appeared at the
wrong world location.
Now the polygon and planes are transformed with the full inverse of the
object's placement matrix, and the plane normals are also rotated. The
local mesh is therefore aligned with the object's local axes and appears
in the correct world position when the placement is applied.
Added regression tests for Space 5710 (rotated placement) and Space 2363
(identity placement) using the real HouseWithGarage_AC22_IFC2X3.ifc
fixture.
Generated with the assistance of an AI coding tool.
Two fixes for space generation:
1. IFC2X3 schema: build_brep_space now falls back to plain
IfcRelSpaceBoundary because IfcRelSpaceBoundary1stLevel does not exist in
IFC2X3.
2. Geometry location: set_space_representation_from_polygon now aligns the
IFC ObjectPlacement with the Blender object, converts base_z/planes and
the footprint polygon to the object's local coordinate system before
building, and fixes the base_z unit scale. The centred-cube regeneration
test was updated to check world bounds because the mesh is now placed
relative to the object placement.
Generated with the assistance of an AI coding tool.
The extrusion height was capped at the top/bottom plane anchor z (the mean
of the ray-cast hits, near the footprint centre), so a sloped ceiling's
high side stopped short of the plane (e.g. 5.5 m instead of 6.88 m for the
shed roof test). Extend the extrusion to the plane's z at every footprint
vertex before clipping, falling back to the base z as before. Also correct
the mirrored profile-to-world mapping comment in the shed roof test helper
(the ridge is at world y=-5, not y=+5).
Generated with the assistance of an AI coding tool.
Cover a curved vertical wall (EXTRUDE_CLIP strategy) in test_space.py and a
sloped slab (clipped extrusion) in test_spatial.py.
Generated with the assistance of an AI coding tool.
Thread the footprint-query bounding walls and container into the
set_space_representation_from_polygon dispatcher and add an end-to-end
test for a space under a shed roof with a sloped underside.
Generated with the assistance of an AI coding tool.
Dispatch on EXTRUDE_CLIP vs B-rep when building space volumes, and
fix fixture placement and visibility bugs in the spatial tests.
Generated with the assistance of an AI coding tool.
Build a faceted B-rep space from auto-generated boundary faces when
sloped or curved bounding elements make a clipped extrusion
unsuitable.
Generated with the assistance of an AI coding tool.
Allow sloped roof/slab/wall faces to bound space faces when the
strict anti-parallel rule leaves a face uncovered, using a
footprint-scaled distance tolerance. Existing matching behaviour
is preserved (fallback-only).
Generated with the assistance of an AI coding tool.
Detect whether a space can be built as a clipped extrusion or needs
a B-rep fallback, based on wall face orientation and top/bottom
bounding planes.
Generated with the assistance of an AI coding tool.
get_vertical_bounding_planes always returns EXTRUDE_CLIP; the
EXTRUDE_CLIP-vs-BREP decision belongs to the calling layer.
Generated with the assistance of an AI coding tool.
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.