Add geometry.clip_solid API and fix half_space_solid docstring

clip_solid(file, item, location, normal) wraps Clipping.apply() to
clip any solid with a half-space plane. Normal points toward the
removed material — same convention as add_wall_representation clippings.

Clarifies that agreement_flag=False means the normal direction is the
void (removed) side.
This commit is contained in:
Bruno Postle
2026-03-12 21:30:39 +00:00
parent 75cfbd40b1
commit 80922fcd9a
4 changed files with 169 additions and 1 deletions
@@ -26,6 +26,7 @@ geometry extrusions).
from .. import wrap_usecases
from .add_axis_representation import add_axis_representation
from .add_boolean import add_boolean
from .clip_solid import clip_solid
from .add_door_representation import add_door_representation
from .add_footprint_representation import add_footprint_representation
from .add_mesh_representation import add_mesh_representation
@@ -61,6 +62,7 @@ wrap_usecases(__path__, __name__)
__all__ = [
"add_axis_representation",
"add_boolean",
"clip_solid",
"add_door_representation",
"add_footprint_representation",
"add_mesh_representation",
@@ -0,0 +1,62 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Sequence
import ifcopenshell.util.unit
from ifcopenshell.util.data import Clipping
def clip_solid(
file: ifcopenshell.file,
item: ifcopenshell.entity_instance,
location: Sequence[float],
normal: Sequence[float],
) -> ifcopenshell.entity_instance:
"""Clip a solid with a half-space plane, returning an IfcBooleanClippingResult.
Convenience wrapper around :class:`ifcopenshell.util.data.Clipping` for
use with any solid. The ``normal`` points toward the **removed** material
(the void side); the kept region is on the opposite side. This is the same
convention used by the ``clippings`` parameter of :func:`add_wall_representation`.
After clipping, set the parent ``IfcShapeRepresentation``
``RepresentationType`` to ``"Clipping"``.
Example — trim an extruded solid to a lean-to slope (removed material is
above the slope)::
bcr = ifcopenshell.api.run(
"geometry.clip_solid", model,
item=extrusion,
location=[0.0, 0.0, 3.26],
normal=[0.419, 0.0, 0.908], # points UP toward removed material
)
:param item: The solid to clip (``IfcSweptAreaSolid``, ``IfcSweptDiskSolid``,
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.
: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)
@@ -1038,7 +1038,7 @@ class ShapeBuilder:
) -> ifcopenshell.entity_instance:
"""
:param plane: The IfcPlane representing the half space.
:param agreement_flag: False if +Z represents the void
:param agreement_flag: If False (default), the plane normal points toward the **removed** material (the void). The kept region is on the opposite side from the normal.
:return: IfcHalfSpaceSolid
"""
return self.file.createIfcHalfSpaceSolid(plane, AgreementFlag=agreement_flag)
@@ -0,0 +1,104 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# 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 ifcopenshell.api.geometry
import ifcopenshell.util.shape_builder
import test.bootstrap
class TestClipSolid(test.bootstrap.IFC4):
def make_extrusion(self):
builder = ifcopenshell.util.shape_builder.ShapeBuilder(self.file)
rect = builder.rectangle(size=(1.0, 1.0))
return builder.extrude(rect, magnitude=4.0)
def test_returns_boolean_clipping_result(self):
extrusion = self.make_extrusion()
result = ifcopenshell.api.geometry.clip_solid(
self.file,
item=extrusion,
location=[0.0, 0.0, 3.0],
normal=[0.0, 0.0, 1.0],
)
assert result.is_a("IfcBooleanClippingResult")
assert result.Operator == "DIFFERENCE"
def test_first_operand_is_the_item(self):
extrusion = self.make_extrusion()
result = ifcopenshell.api.geometry.clip_solid(
self.file,
item=extrusion,
location=[0.0, 0.0, 3.0],
normal=[0.0, 0.0, 1.0],
)
assert result.FirstOperand == extrusion
def test_second_operand_is_half_space_solid(self):
extrusion = self.make_extrusion()
result = ifcopenshell.api.geometry.clip_solid(
self.file,
item=extrusion,
location=[0.0, 0.0, 3.0],
normal=[0.0, 0.0, 1.0],
)
assert result.SecondOperand.is_a("IfcHalfSpaceSolid")
def test_clip_plane_location_matches(self):
extrusion = self.make_extrusion()
result = ifcopenshell.api.geometry.clip_solid(
self.file,
item=extrusion,
location=[0.0, 0.0, 3.0],
normal=[0.0, 0.0, 1.0],
)
plane = result.SecondOperand.BaseSurface
coords = plane.Position.Location.Coordinates
assert list(coords) == [0.0, 0.0, 3.0]
def test_chaining_two_clips(self):
extrusion = self.make_extrusion()
first_clip = ifcopenshell.api.geometry.clip_solid(
self.file,
item=extrusion,
location=[0.0, 0.0, 3.0],
normal=[0.0, 0.0, 1.0],
)
second_clip = ifcopenshell.api.geometry.clip_solid(
self.file,
item=first_clip,
location=[0.0, 0.0, 1.0],
normal=[0.0, 0.0, -1.0],
)
assert second_clip.is_a("IfcBooleanClippingResult")
assert second_clip.FirstOperand == first_clip
assert first_clip.FirstOperand == extrusion
def test_angled_clip_plane(self):
extrusion = self.make_extrusion()
result = ifcopenshell.api.geometry.clip_solid(
self.file,
item=extrusion,
location=[0.0, 0.0, 3.26],
normal=[0.419, 0.0, 0.908],
)
assert result.is_a("IfcBooleanClippingResult")
assert result.SecondOperand.is_a("IfcHalfSpaceSolid")
class TestClipSolidIFC2X3(test.bootstrap.IFC2X3, TestClipSolid):
pass