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.
This commit is contained in:
Bruno Postle
2026-03-20 21:29:46 +00:00
parent c3ca969e32
commit 7197d79dd9
5 changed files with 127 additions and 5 deletions
@@ -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
@@ -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
@@ -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).
@@ -16,7 +16,10 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -16,7 +16,10 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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