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