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 __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 import ifcopenshell.util.unit
from ifcopenshell.util.data import Clipping from ifcopenshell.util.data import Clipping
@@ -61,9 +64,23 @@ def clip_solid(
or ``IfcBooleanClippingResult``). or ``IfcBooleanClippingResult``).
:param location: A point on the clipping plane in the representation's :param location: A point on the clipping plane in the representation's
local coordinate system. 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``. :return: The resulting ``IfcBooleanClippingResult``.
""" """
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
clipping = Clipping(location=tuple(location), normal=tuple(normal)) 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 __future__ import annotations
from typing import Sequence import json
from typing import Optional, Sequence
import numpy as np import numpy as np
import ifcopenshell.api.pset
import ifcopenshell.util.element
import ifcopenshell.util.unit import ifcopenshell.util.unit
from ifcopenshell.util.shape_builder import ShapeBuilder from ifcopenshell.util.shape_builder import ShapeBuilder
@@ -33,6 +36,7 @@ def clip_solid_bounded(
normal: Sequence[float], normal: Sequence[float],
boundary_points: Sequence[Sequence[float]], boundary_points: Sequence[Sequence[float]],
boundary_position: Sequence[float] = (0.0, 0.0, 0.0), boundary_position: Sequence[float] = (0.0, 0.0, 0.0),
element: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance: ) -> ifcopenshell.entity_instance:
"""Clip a solid with a polygonally bounded half-space, returning an IfcBooleanClippingResult. """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. is automatically closed — do not repeat the first point.
:param boundary_position: 3D origin of the boundary coordinate system :param boundary_position: 3D origin of the boundary coordinate system
(axes default to the global X/Y/Z directions). Defaults to the origin. (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``. :return: The resulting ``IfcBooleanClippingResult``.
""" """
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
@@ -96,4 +103,14 @@ def clip_solid_bounded(
boundary = file.createIfcPolyline(ifc_pts) boundary = file.createIfcPolyline(ifc_pts)
half_space = file.create_entity("IfcPolygonalBoundedHalfSpace", plane, False, boundary_pos_entity, boundary) 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 additional extrusions are generated for each connection that boolean
difference the base extrusion. 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 This will also update the axis line representation (e.g. trim the axis line
to any connections). to any connections).
@@ -16,7 +16,10 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. # along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import json
import ifcopenshell.api.geometry import ifcopenshell.api.geometry
import ifcopenshell.util.element
import ifcopenshell.util.shape_builder import ifcopenshell.util.shape_builder
import test.bootstrap import test.bootstrap
@@ -100,5 +103,53 @@ class TestClipSolid(test.bootstrap.IFC4):
assert result.SecondOperand.is_a("IfcHalfSpaceSolid") 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): class TestClipSolidIFC2X3(test.bootstrap.IFC2X3, TestClipSolid):
pass pass
@@ -16,7 +16,10 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. # along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import json
import ifcopenshell.api.geometry import ifcopenshell.api.geometry
import ifcopenshell.util.element
import ifcopenshell.util.shape_builder import ifcopenshell.util.shape_builder
import test.bootstrap import test.bootstrap
@@ -145,5 +148,33 @@ class TestClipSolidBounded(test.bootstrap.IFC4):
assert first_clip.FirstOperand == extrusion 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): class TestClipSolidBoundedIFC2X3(test.bootstrap.IFC2X3, TestClipSolidBounded):
pass pass