api.boundary.edit_attributes: add PhysicalOrVirtualBoundary and InternalOrExternalBoundary params

Both attributes are required by the IFC schema but were not settable via
the API function. Add physical_or_virtual and internal_or_external parameters
with "NOTDEFINED" defaults for backward compatibility. Update Bonsai boundary
panel to expose both fields in the editor.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Bruno Postle
2026-03-30 07:25:22 +01:00
parent c214d255c9
commit be05d771a2
5 changed files with 175 additions and 9 deletions
@@ -377,6 +377,8 @@ class EnableEditingBoundary(bpy.types.Operator):
obj = tool.Ifc.get_object(entity)
if entity and obj:
setattr(bprops, blender_property, obj)
bprops.physical_or_virtual = boundary.PhysicalOrVirtualBoundary or "NOTDEFINED"
bprops.internal_or_external = boundary.InternalOrExternalBoundary or "NOTDEFINED"
return {"FINISHED"}
@@ -392,6 +394,8 @@ class DisableEditingBoundary(bpy.types.Operator):
bprops.is_editing = False
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
setattr(bprops, blender_property, None)
bprops.physical_or_virtual = "NOTDEFINED"
bprops.internal_or_external = "NOTDEFINED"
return {"FINISHED"}
@@ -411,6 +415,8 @@ class EditBoundaryAttributes(bpy.types.Operator, tool.Ifc.Operator):
obj = getattr(bprops, blender_property, None)
entity = tool.Ifc.get_entity(obj)
attributes[blender_property] = entity
attributes["physical_or_virtual"] = bprops.physical_or_virtual
attributes["internal_or_external"] = bprops.internal_or_external
ifcopenshell.api.boundary.edit_attributes(tool.Ifc.get(), entity=boundary, **attributes)
bpy.ops.bim.disable_editing_boundary()
return {"FINISHED"}
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Union
import bpy
from bpy.props import (
BoolProperty,
EnumProperty,
PointerProperty,
)
from bpy.types import PropertyGroup
@@ -50,12 +51,43 @@ def element_filter(self: "BIMObjectBoundaryProperties", object: bpy.types.Object
return False
def get_internal_or_external_items(
self: "BIMObjectBoundaryProperties", context: bpy.types.Context | None
) -> list[tuple[str, str, str]]:
items = [
("INTERNAL", "Internal", ""),
("EXTERNAL", "External", ""),
]
ifc = tool.Ifc.get()
if not ifc or ifc.schema != "IFC2X3":
items += [
("EXTERNAL_EARTH", "External Earth", ""),
("EXTERNAL_WATER", "External Water", ""),
("EXTERNAL_FIRE", "External Fire", ""),
]
items.append(("NOTDEFINED", "Not Defined", ""))
return items
class BIMObjectBoundaryProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
relating_space: PointerProperty(name="RelatingSpace", type=bpy.types.Object, poll=space_filter)
related_building_element: PointerProperty(name="RelatedBuildingElement", type=bpy.types.Object, poll=element_filter)
parent_boundary: PointerProperty(name="ParentBoundary", type=bpy.types.Object, poll=boundary_filter)
corresponding_boundary: PointerProperty(name="CorrespondingBoundary", type=bpy.types.Object, poll=boundary_filter)
physical_or_virtual: EnumProperty(
name="PhysicalOrVirtualBoundary",
items=[
("PHYSICAL", "Physical", ""),
("VIRTUAL", "Virtual", ""),
("NOTDEFINED", "Not Defined", ""),
],
default="NOTDEFINED",
)
internal_or_external: EnumProperty(
name="InternalOrExternalBoundary",
items=get_internal_or_external_items,
)
if TYPE_CHECKING:
is_editing: bool
@@ -63,6 +95,8 @@ class BIMObjectBoundaryProperties(PropertyGroup):
related_building_element: Union[bpy.types.Object, None]
parent_boundary: Union[bpy.types.Object, None]
corresponding_boundary: Union[bpy.types.Object, None]
physical_or_virtual: str
internal_or_external: str # values depend on schema: IFC2X3 omits EXTERNAL_EARTH/WATER/FIRE
class BIMBoundaryProperties(PropertyGroup):
@@ -77,6 +77,10 @@ class BIM_PT_Boundary(Panel):
self.draw_relation_editor(boundary, "RelatedBuildingElement", "related_building_element")
self.draw_relation_editor(boundary, "ParentBoundary", "parent_boundary")
self.draw_relation_editor(boundary, "CorrespondingBoundary", "corresponding_boundary")
row = self.layout.row()
row.prop(self.bprops, "physical_or_virtual")
row = self.layout.row()
row.prop(self.bprops, "internal_or_external")
else:
row = self.layout.row()
row.operator("bim.enable_editing_boundary", icon="GREASEPENCIL", text="Edit")
@@ -84,6 +88,8 @@ class BIM_PT_Boundary(Panel):
self.draw_relation_data(boundary, "RelatedBuildingElement")
self.draw_relation_data(boundary, "ParentBoundary")
self.draw_relation_data(boundary, "CorrespondingBoundary")
self.draw_enum_data(boundary, "PhysicalOrVirtualBoundary")
self.draw_enum_data(boundary, "InternalOrExternalBoundary")
if hasattr(boundary, "InnerBoundaries"):
for i, inner_boundary in enumerate(getattr(boundary, "InnerBoundaries", ())):
row = self.layout.row(align=True)
@@ -110,6 +116,11 @@ class BIM_PT_Boundary(Panel):
else:
row.label(text="")
def draw_enum_data(self, boundary, ifc_attribute: str):
row = self.layout.row(align=True)
row.label(text=ifc_attribute)
row.label(text=getattr(boundary, ifc_attribute, "") or "")
def draw_relation_editor(self, boundary, ifc_attribute: str, blender_property: str):
if hasattr(boundary, ifc_attribute):
row = self.layout.row(align=True)
@@ -27,12 +27,11 @@ def edit_attributes(
related_building_element: ifcopenshell.entity_instance,
parent_boundary: Optional[ifcopenshell.entity_instance] = None,
corresponding_boundary: Optional[ifcopenshell.entity_instance] = None,
physical_or_virtual: str = "NOTDEFINED",
internal_or_external: str = "NOTDEFINED",
) -> None:
"""Modify the relationships of a space boundary relationship
Currently this function is quite minimal and offers no advantage to
manual assignment of the space boundary attributes.
:param entity: The IfcRelSpaceBoundary to modify
:param relating_space: The IfcSpace or IfcExternalSpatialElement that
the space boundary is related to.
@@ -44,17 +43,18 @@ def edit_attributes(
:param corresponding_boundary: The other IfcRelSpaceBoundary on the
other side of the related element. The pair together represents a
thermal boundary. This only applies to 2nd level boundaries.
:param physical_or_virtual: IfcPhysicalOrVirtualEnum value: "PHYSICAL",
"VIRTUAL", or "NOTDEFINED".
:param internal_or_external: IfcInternalOrExternalEnum value:
"INTERNAL", "EXTERNAL", "EXTERNAL_EARTH", "EXTERNAL_WATER",
"EXTERNAL_FIRE", or "NOTDEFINED".
:return: None
"""
entity = entity
relating_space = relating_space
related_building_element = related_building_element
parent_boundary = parent_boundary
corresponding_boundary = corresponding_boundary
entity.RelatingSpace = relating_space
entity.RelatedBuildingElement = related_building_element
if hasattr(entity, "ParentBoundary"):
entity.ParentBoundary = parent_boundary
if hasattr(entity, "CorrespondingBoundary"):
entity.CorrespondingBoundary = corresponding_boundary
entity.PhysicalOrVirtualBoundary = physical_or_virtual
entity.InternalOrExternalBoundary = internal_or_external
@@ -0,0 +1,115 @@
# 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/>.
# This file was generated with the assistance of an AI coding tool.
import ifcopenshell.api.boundary
import ifcopenshell.api.root
import test.bootstrap
class TestEditAttributes(test.bootstrap.IFC4):
def setup_boundary(self):
space = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcSpace")
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
boundary = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcRelSpaceBoundary")
return boundary, space, wall
def test_sets_relating_space_and_building_element(self):
boundary, space, wall = self.setup_boundary()
ifcopenshell.api.boundary.edit_attributes(
self.file, entity=boundary, relating_space=space, related_building_element=wall
)
assert boundary.RelatingSpace == space
assert boundary.RelatedBuildingElement == wall
def test_defaults_enums_to_notdefined(self):
boundary, space, wall = self.setup_boundary()
ifcopenshell.api.boundary.edit_attributes(
self.file, entity=boundary, relating_space=space, related_building_element=wall
)
assert boundary.PhysicalOrVirtualBoundary == "NOTDEFINED"
assert boundary.InternalOrExternalBoundary == "NOTDEFINED"
def test_sets_physical_or_virtual(self):
boundary, space, wall = self.setup_boundary()
ifcopenshell.api.boundary.edit_attributes(
self.file,
entity=boundary,
relating_space=space,
related_building_element=wall,
physical_or_virtual="PHYSICAL",
)
assert boundary.PhysicalOrVirtualBoundary == "PHYSICAL"
def test_sets_internal_or_external(self):
boundary, space, wall = self.setup_boundary()
ifcopenshell.api.boundary.edit_attributes(
self.file,
entity=boundary,
relating_space=space,
related_building_element=wall,
internal_or_external="EXTERNAL",
)
assert boundary.InternalOrExternalBoundary == "EXTERNAL"
def test_sets_all_enum_variants(self):
boundary, space, wall = self.setup_boundary()
for value in ("PHYSICAL", "VIRTUAL", "NOTDEFINED"):
ifcopenshell.api.boundary.edit_attributes(
self.file,
entity=boundary,
relating_space=space,
related_building_element=wall,
physical_or_virtual=value,
)
assert boundary.PhysicalOrVirtualBoundary == value
for value in ("INTERNAL", "EXTERNAL", "EXTERNAL_EARTH", "EXTERNAL_WATER", "EXTERNAL_FIRE", "NOTDEFINED"):
ifcopenshell.api.boundary.edit_attributes(
self.file,
entity=boundary,
relating_space=space,
related_building_element=wall,
internal_or_external=value,
)
assert boundary.InternalOrExternalBoundary == value
class TestEditAttributesIFC2X3(test.bootstrap.IFC2X3, TestEditAttributes):
def test_sets_all_enum_variants(self):
boundary, space, wall = self.setup_boundary()
for value in ("PHYSICAL", "VIRTUAL", "NOTDEFINED"):
ifcopenshell.api.boundary.edit_attributes(
self.file,
entity=boundary,
relating_space=space,
related_building_element=wall,
physical_or_virtual=value,
)
assert boundary.PhysicalOrVirtualBoundary == value
# IFC2X3 only has INTERNAL, EXTERNAL, NOTDEFINED
for value in ("INTERNAL", "EXTERNAL", "NOTDEFINED"):
ifcopenshell.api.boundary.edit_attributes(
self.file,
entity=boundary,
relating_space=space,
related_building_element=wall,
internal_or_external=value,
)
assert boundary.InternalOrExternalBoundary == value