From 7197d79dd98f458c1e9a18a1773a6d6bfb90209b Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Fri, 20 Mar 2026 21:29:46 +0000 Subject: [PATCH] clip_solid/clip_solid_bounded: add element param to register BBIM_Boolean Add an optional element parameter to geometry.clip_solid and geometry.clip_solid_bounded. When provided, the resulting IfcBooleanClippingResult is registered in the element's BBIM_Boolean property set so that regenerate_wall_representation preserves it during regeneration. Generated with the assistance of an AI coding tool. --- .../ifcopenshell/api/geometry/clip_solid.py | 23 +++++++-- .../api/geometry/clip_solid_bounded.py | 21 +++++++- .../regenerate_wall_representation.py | 6 +++ .../test/api/geometry/test_clip_solid.py | 51 +++++++++++++++++++ .../api/geometry/test_clip_solid_bounded.py | 31 +++++++++++ 5 files changed, 127 insertions(+), 5 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/clip_solid.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/clip_solid.py index 32f8fc0c96..a83fd4385f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/clip_solid.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/clip_solid.py @@ -18,8 +18,11 @@ from __future__ import annotations -from typing import Sequence +import json +from typing import Optional, Sequence +import ifcopenshell.api.pset +import ifcopenshell.util.element import ifcopenshell.util.unit from ifcopenshell.util.data import Clipping @@ -61,9 +64,23 @@ def clip_solid( or ``IfcBooleanClippingResult``). :param location: A point on the clipping plane in the representation's local coordinate system. - :param normal: Plane normal pointing toward the material to be removed. + :param normal: Plane normal pointing toward the material to be removed + (see warning above). + :param element: If provided, the resulting ``IfcBooleanClippingResult`` is + registered in the element's ``BBIM_Boolean`` property set so that + :func:`regenerate_wall_representation` preserves it during regeneration. :return: The resulting ``IfcBooleanClippingResult``. """ unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) clipping = Clipping(location=tuple(location), normal=tuple(normal)) - return clipping.apply(file, item, unit_scale) + result = clipping.apply(file, item, unit_scale) + if element is not None: + pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Boolean") + if pset_data: + pset = file.by_id(pset_data["id"]) + data = list(set(json.loads(pset_data["Data"]) + [result.id()])) + else: + pset = ifcopenshell.api.pset.add_pset(file, product=element, name="BBIM_Boolean") + data = [result.id()] + ifcopenshell.api.pset.edit_pset(file, pset=pset, properties={"Data": json.dumps(data)}) + return result diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/clip_solid_bounded.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/clip_solid_bounded.py index 1df242eb28..ac2e39c741 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/clip_solid_bounded.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/clip_solid_bounded.py @@ -18,10 +18,13 @@ from __future__ import annotations -from typing import Sequence +import json +from typing import Optional, Sequence import numpy as np +import ifcopenshell.api.pset +import ifcopenshell.util.element import ifcopenshell.util.unit from ifcopenshell.util.shape_builder import ShapeBuilder @@ -33,6 +36,7 @@ def clip_solid_bounded( normal: Sequence[float], boundary_points: Sequence[Sequence[float]], boundary_position: Sequence[float] = (0.0, 0.0, 0.0), + element: Optional[ifcopenshell.entity_instance] = None, ) -> ifcopenshell.entity_instance: """Clip a solid with a polygonally bounded half-space, returning an IfcBooleanClippingResult. @@ -67,6 +71,9 @@ def clip_solid_bounded( is automatically closed — do not repeat the first point. :param boundary_position: 3D origin of the boundary coordinate system (axes default to the global X/Y/Z directions). Defaults to the origin. + :param element: If provided, the resulting ``IfcBooleanClippingResult`` is + registered in the element's ``BBIM_Boolean`` property set so that + :func:`regenerate_wall_representation` preserves it during regeneration. :return: The resulting ``IfcBooleanClippingResult``. """ unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) @@ -96,4 +103,14 @@ def clip_solid_bounded( boundary = file.createIfcPolyline(ifc_pts) half_space = file.create_entity("IfcPolygonalBoundedHalfSpace", plane, False, boundary_pos_entity, boundary) - return file.create_entity("IfcBooleanClippingResult", "DIFFERENCE", item, half_space) + result = file.create_entity("IfcBooleanClippingResult", "DIFFERENCE", item, half_space) + if element is not None: + pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Boolean") + if pset_data: + pset = file.by_id(pset_data["id"]) + data = list(set(json.loads(pset_data["Data"]) + [result.id()])) + else: + pset = ifcopenshell.api.pset.add_pset(file, product=element, name="BBIM_Boolean") + data = [result.id()] + ifcopenshell.api.pset.edit_pset(file, pset=pset, properties={"Data": json.dumps(data)}) + return result diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py index 9143577438..e59c6e1efa 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py @@ -69,6 +69,12 @@ def regenerate_wall_representation( additional extrusions are generated for each connection that boolean difference the base extrusion. + Clippings applied via :func:`geometry.clip_solid` or + :func:`geometry.clip_solid_bounded` are preserved only if the ``element`` + parameter was passed when creating them, which registers the result in the + ``BBIM_Boolean`` property set. Clippings created without that parameter + are silently discarded during regeneration. + This will also update the axis line representation (e.g. trim the axis line to any connections). diff --git a/src/ifcopenshell-python/test/api/geometry/test_clip_solid.py b/src/ifcopenshell-python/test/api/geometry/test_clip_solid.py index 93a161ac4c..fa7a493cab 100644 --- a/src/ifcopenshell-python/test/api/geometry/test_clip_solid.py +++ b/src/ifcopenshell-python/test/api/geometry/test_clip_solid.py @@ -16,7 +16,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import json + import ifcopenshell.api.geometry +import ifcopenshell.util.element import ifcopenshell.util.shape_builder import test.bootstrap @@ -100,5 +103,53 @@ class TestClipSolid(test.bootstrap.IFC4): assert result.SecondOperand.is_a("IfcHalfSpaceSolid") + def test_element_registers_result_in_bbim_boolean(self): + extrusion = self.make_extrusion() + wall = self.file.createIfcWall() + result = ifcopenshell.api.geometry.clip_solid( + self.file, + item=extrusion, + location=[0.0, 0.0, 3.0], + normal=[0.0, 0.0, 1.0], + element=wall, + ) + pset = ifcopenshell.util.element.get_pset(wall, "BBIM_Boolean") + assert pset is not None + assert result.id() in json.loads(pset["Data"]) + + def test_element_appends_to_existing_bbim_boolean(self): + extrusion = self.make_extrusion() + wall = self.file.createIfcWall() + first = ifcopenshell.api.geometry.clip_solid( + self.file, + item=extrusion, + location=[0.0, 0.0, 3.0], + normal=[0.0, 0.0, 1.0], + element=wall, + ) + second = ifcopenshell.api.geometry.clip_solid( + self.file, + item=first, + location=[0.0, 0.0, 1.0], + normal=[0.0, 0.0, -1.0], + element=wall, + ) + pset = ifcopenshell.util.element.get_pset(wall, "BBIM_Boolean") + ids = json.loads(pset["Data"]) + assert first.id() in ids + assert second.id() in ids + + def test_no_element_does_not_create_pset(self): + extrusion = self.make_extrusion() + wall = self.file.createIfcWall() + ifcopenshell.api.geometry.clip_solid( + self.file, + item=extrusion, + location=[0.0, 0.0, 3.0], + normal=[0.0, 0.0, 1.0], + ) + assert ifcopenshell.util.element.get_pset(wall, "BBIM_Boolean") is None + + class TestClipSolidIFC2X3(test.bootstrap.IFC2X3, TestClipSolid): pass diff --git a/src/ifcopenshell-python/test/api/geometry/test_clip_solid_bounded.py b/src/ifcopenshell-python/test/api/geometry/test_clip_solid_bounded.py index 0ec448e5f5..7e79400011 100644 --- a/src/ifcopenshell-python/test/api/geometry/test_clip_solid_bounded.py +++ b/src/ifcopenshell-python/test/api/geometry/test_clip_solid_bounded.py @@ -16,7 +16,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import json + import ifcopenshell.api.geometry +import ifcopenshell.util.element import ifcopenshell.util.shape_builder import test.bootstrap @@ -145,5 +148,33 @@ class TestClipSolidBounded(test.bootstrap.IFC4): assert first_clip.FirstOperand == extrusion + def test_element_registers_result_in_bbim_boolean(self): + extrusion = self.make_extrusion() + wall = self.file.createIfcWall() + result = ifcopenshell.api.geometry.clip_solid_bounded( + self.file, + item=extrusion, + location=[2.5, 0.0, 2.0], + normal=[0.6, 0.0, 0.8], + boundary_points=[[2.0, 0.0], [3.0, 0.0], [3.0, 2.0], [2.0, 2.0]], + element=wall, + ) + pset = ifcopenshell.util.element.get_pset(wall, "BBIM_Boolean") + assert pset is not None + assert result.id() in json.loads(pset["Data"]) + + def test_no_element_does_not_create_pset(self): + extrusion = self.make_extrusion() + wall = self.file.createIfcWall() + ifcopenshell.api.geometry.clip_solid_bounded( + self.file, + item=extrusion, + location=[2.5, 0.0, 2.0], + normal=[0.6, 0.0, 0.8], + boundary_points=[[2.0, 0.0], [3.0, 0.0], [3.0, 2.0], [2.0, 2.0]], + ) + assert ifcopenshell.util.element.get_pset(wall, "BBIM_Boolean") is None + + class TestClipSolidBoundedIFC2X3(test.bootstrap.IFC2X3, TestClipSolidBounded): pass