clip_solid/clip_solid_bounded: implement element param to register BBIM_Boolean

This commit is contained in:
Bruno Postle
2026-03-23 22:35:49 +00:00
parent 020ef09a0c
commit 4a0cb36b2f
4 changed files with 121 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
@@ -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