Skip to content

Commit be05d77

Browse files
committed
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.
1 parent c214d25 commit be05d77

5 files changed

Lines changed: 175 additions & 9 deletions

File tree

src/bonsai/bonsai/bim/module/boundary/operator.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,8 @@ def execute(self, context):
377377
obj = tool.Ifc.get_object(entity)
378378
if entity and obj:
379379
setattr(bprops, blender_property, obj)
380+
bprops.physical_or_virtual = boundary.PhysicalOrVirtualBoundary or "NOTDEFINED"
381+
bprops.internal_or_external = boundary.InternalOrExternalBoundary or "NOTDEFINED"
380382
return {"FINISHED"}
381383

382384

@@ -392,6 +394,8 @@ def execute(self, context):
392394
bprops.is_editing = False
393395
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
394396
setattr(bprops, blender_property, None)
397+
bprops.physical_or_virtual = "NOTDEFINED"
398+
bprops.internal_or_external = "NOTDEFINED"
395399
return {"FINISHED"}
396400

397401

@@ -411,6 +415,8 @@ def _execute(self, context):
411415
obj = getattr(bprops, blender_property, None)
412416
entity = tool.Ifc.get_entity(obj)
413417
attributes[blender_property] = entity
418+
attributes["physical_or_virtual"] = bprops.physical_or_virtual
419+
attributes["internal_or_external"] = bprops.internal_or_external
414420
ifcopenshell.api.boundary.edit_attributes(tool.Ifc.get(), entity=boundary, **attributes)
415421
bpy.ops.bim.disable_editing_boundary()
416422
return {"FINISHED"}

src/bonsai/bonsai/bim/module/boundary/prop.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import bpy
2222
from bpy.props import (
2323
BoolProperty,
24+
EnumProperty,
2425
PointerProperty,
2526
)
2627
from bpy.types import PropertyGroup
@@ -50,19 +51,52 @@ def element_filter(self: "BIMObjectBoundaryProperties", object: bpy.types.Object
5051
return False
5152

5253

54+
def get_internal_or_external_items(
55+
self: "BIMObjectBoundaryProperties", context: bpy.types.Context | None
56+
) -> list[tuple[str, str, str]]:
57+
items = [
58+
("INTERNAL", "Internal", ""),
59+
("EXTERNAL", "External", ""),
60+
]
61+
ifc = tool.Ifc.get()
62+
if not ifc or ifc.schema != "IFC2X3":
63+
items += [
64+
("EXTERNAL_EARTH", "External Earth", ""),
65+
("EXTERNAL_WATER", "External Water", ""),
66+
("EXTERNAL_FIRE", "External Fire", ""),
67+
]
68+
items.append(("NOTDEFINED", "Not Defined", ""))
69+
return items
70+
71+
5372
class BIMObjectBoundaryProperties(PropertyGroup):
5473
is_editing: BoolProperty(name="Is Editing")
5574
relating_space: PointerProperty(name="RelatingSpace", type=bpy.types.Object, poll=space_filter)
5675
related_building_element: PointerProperty(name="RelatedBuildingElement", type=bpy.types.Object, poll=element_filter)
5776
parent_boundary: PointerProperty(name="ParentBoundary", type=bpy.types.Object, poll=boundary_filter)
5877
corresponding_boundary: PointerProperty(name="CorrespondingBoundary", type=bpy.types.Object, poll=boundary_filter)
78+
physical_or_virtual: EnumProperty(
79+
name="PhysicalOrVirtualBoundary",
80+
items=[
81+
("PHYSICAL", "Physical", ""),
82+
("VIRTUAL", "Virtual", ""),
83+
("NOTDEFINED", "Not Defined", ""),
84+
],
85+
default="NOTDEFINED",
86+
)
87+
internal_or_external: EnumProperty(
88+
name="InternalOrExternalBoundary",
89+
items=get_internal_or_external_items,
90+
)
5991

6092
if TYPE_CHECKING:
6193
is_editing: bool
6294
relating_space: Union[bpy.types.Object, None]
6395
related_building_element: Union[bpy.types.Object, None]
6496
parent_boundary: Union[bpy.types.Object, None]
6597
corresponding_boundary: Union[bpy.types.Object, None]
98+
physical_or_virtual: str
99+
internal_or_external: str # values depend on schema: IFC2X3 omits EXTERNAL_EARTH/WATER/FIRE
66100

67101

68102
class BIMBoundaryProperties(PropertyGroup):

src/bonsai/bonsai/bim/module/boundary/ui.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,19 @@ def draw(self, context):
7777
self.draw_relation_editor(boundary, "RelatedBuildingElement", "related_building_element")
7878
self.draw_relation_editor(boundary, "ParentBoundary", "parent_boundary")
7979
self.draw_relation_editor(boundary, "CorrespondingBoundary", "corresponding_boundary")
80+
row = self.layout.row()
81+
row.prop(self.bprops, "physical_or_virtual")
82+
row = self.layout.row()
83+
row.prop(self.bprops, "internal_or_external")
8084
else:
8185
row = self.layout.row()
8286
row.operator("bim.enable_editing_boundary", icon="GREASEPENCIL", text="Edit")
8387
self.draw_relation_data(boundary, "RelatingSpace")
8488
self.draw_relation_data(boundary, "RelatedBuildingElement")
8589
self.draw_relation_data(boundary, "ParentBoundary")
8690
self.draw_relation_data(boundary, "CorrespondingBoundary")
91+
self.draw_enum_data(boundary, "PhysicalOrVirtualBoundary")
92+
self.draw_enum_data(boundary, "InternalOrExternalBoundary")
8793
if hasattr(boundary, "InnerBoundaries"):
8894
for i, inner_boundary in enumerate(getattr(boundary, "InnerBoundaries", ())):
8995
row = self.layout.row(align=True)
@@ -110,6 +116,11 @@ def draw_relation_data(self, boundary, ifc_attribute: str):
110116
else:
111117
row.label(text="")
112118

119+
def draw_enum_data(self, boundary, ifc_attribute: str):
120+
row = self.layout.row(align=True)
121+
row.label(text=ifc_attribute)
122+
row.label(text=getattr(boundary, ifc_attribute, "") or "")
123+
113124
def draw_relation_editor(self, boundary, ifc_attribute: str, blender_property: str):
114125
if hasattr(boundary, ifc_attribute):
115126
row = self.layout.row(align=True)

src/ifcopenshell-python/ifcopenshell/api/boundary/edit_attributes.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,11 @@ def edit_attributes(
2727
related_building_element: ifcopenshell.entity_instance,
2828
parent_boundary: Optional[ifcopenshell.entity_instance] = None,
2929
corresponding_boundary: Optional[ifcopenshell.entity_instance] = None,
30+
physical_or_virtual: str = "NOTDEFINED",
31+
internal_or_external: str = "NOTDEFINED",
3032
) -> None:
3133
"""Modify the relationships of a space boundary relationship
3234
33-
Currently this function is quite minimal and offers no advantage to
34-
manual assignment of the space boundary attributes.
35-
3635
:param entity: The IfcRelSpaceBoundary to modify
3736
:param relating_space: The IfcSpace or IfcExternalSpatialElement that
3837
the space boundary is related to.
@@ -44,17 +43,18 @@ def edit_attributes(
4443
:param corresponding_boundary: The other IfcRelSpaceBoundary on the
4544
other side of the related element. The pair together represents a
4645
thermal boundary. This only applies to 2nd level boundaries.
46+
:param physical_or_virtual: IfcPhysicalOrVirtualEnum value: "PHYSICAL",
47+
"VIRTUAL", or "NOTDEFINED".
48+
:param internal_or_external: IfcInternalOrExternalEnum value:
49+
"INTERNAL", "EXTERNAL", "EXTERNAL_EARTH", "EXTERNAL_WATER",
50+
"EXTERNAL_FIRE", or "NOTDEFINED".
4751
:return: None
4852
"""
49-
entity = entity
50-
relating_space = relating_space
51-
related_building_element = related_building_element
52-
parent_boundary = parent_boundary
53-
corresponding_boundary = corresponding_boundary
54-
5553
entity.RelatingSpace = relating_space
5654
entity.RelatedBuildingElement = related_building_element
5755
if hasattr(entity, "ParentBoundary"):
5856
entity.ParentBoundary = parent_boundary
5957
if hasattr(entity, "CorrespondingBoundary"):
6058
entity.CorrespondingBoundary = corresponding_boundary
59+
entity.PhysicalOrVirtualBoundary = physical_or_virtual
60+
entity.InternalOrExternalBoundary = internal_or_external
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# IfcOpenShell - IFC toolkit and geometry engine
2+
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.com>
3+
#
4+
# This file is part of IfcOpenShell.
5+
#
6+
# IfcOpenShell is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Lesser General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# IfcOpenShell is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
18+
# This file was generated with the assistance of an AI coding tool.
19+
20+
import ifcopenshell.api.boundary
21+
import ifcopenshell.api.root
22+
import test.bootstrap
23+
24+
25+
class TestEditAttributes(test.bootstrap.IFC4):
26+
def setup_boundary(self):
27+
space = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcSpace")
28+
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
29+
boundary = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcRelSpaceBoundary")
30+
return boundary, space, wall
31+
32+
def test_sets_relating_space_and_building_element(self):
33+
boundary, space, wall = self.setup_boundary()
34+
ifcopenshell.api.boundary.edit_attributes(
35+
self.file, entity=boundary, relating_space=space, related_building_element=wall
36+
)
37+
assert boundary.RelatingSpace == space
38+
assert boundary.RelatedBuildingElement == wall
39+
40+
def test_defaults_enums_to_notdefined(self):
41+
boundary, space, wall = self.setup_boundary()
42+
ifcopenshell.api.boundary.edit_attributes(
43+
self.file, entity=boundary, relating_space=space, related_building_element=wall
44+
)
45+
assert boundary.PhysicalOrVirtualBoundary == "NOTDEFINED"
46+
assert boundary.InternalOrExternalBoundary == "NOTDEFINED"
47+
48+
def test_sets_physical_or_virtual(self):
49+
boundary, space, wall = self.setup_boundary()
50+
ifcopenshell.api.boundary.edit_attributes(
51+
self.file,
52+
entity=boundary,
53+
relating_space=space,
54+
related_building_element=wall,
55+
physical_or_virtual="PHYSICAL",
56+
)
57+
assert boundary.PhysicalOrVirtualBoundary == "PHYSICAL"
58+
59+
def test_sets_internal_or_external(self):
60+
boundary, space, wall = self.setup_boundary()
61+
ifcopenshell.api.boundary.edit_attributes(
62+
self.file,
63+
entity=boundary,
64+
relating_space=space,
65+
related_building_element=wall,
66+
internal_or_external="EXTERNAL",
67+
)
68+
assert boundary.InternalOrExternalBoundary == "EXTERNAL"
69+
70+
def test_sets_all_enum_variants(self):
71+
boundary, space, wall = self.setup_boundary()
72+
for value in ("PHYSICAL", "VIRTUAL", "NOTDEFINED"):
73+
ifcopenshell.api.boundary.edit_attributes(
74+
self.file,
75+
entity=boundary,
76+
relating_space=space,
77+
related_building_element=wall,
78+
physical_or_virtual=value,
79+
)
80+
assert boundary.PhysicalOrVirtualBoundary == value
81+
82+
for value in ("INTERNAL", "EXTERNAL", "EXTERNAL_EARTH", "EXTERNAL_WATER", "EXTERNAL_FIRE", "NOTDEFINED"):
83+
ifcopenshell.api.boundary.edit_attributes(
84+
self.file,
85+
entity=boundary,
86+
relating_space=space,
87+
related_building_element=wall,
88+
internal_or_external=value,
89+
)
90+
assert boundary.InternalOrExternalBoundary == value
91+
92+
93+
class TestEditAttributesIFC2X3(test.bootstrap.IFC2X3, TestEditAttributes):
94+
def test_sets_all_enum_variants(self):
95+
boundary, space, wall = self.setup_boundary()
96+
for value in ("PHYSICAL", "VIRTUAL", "NOTDEFINED"):
97+
ifcopenshell.api.boundary.edit_attributes(
98+
self.file,
99+
entity=boundary,
100+
relating_space=space,
101+
related_building_element=wall,
102+
physical_or_virtual=value,
103+
)
104+
assert boundary.PhysicalOrVirtualBoundary == value
105+
106+
# IFC2X3 only has INTERNAL, EXTERNAL, NOTDEFINED
107+
for value in ("INTERNAL", "EXTERNAL", "NOTDEFINED"):
108+
ifcopenshell.api.boundary.edit_attributes(
109+
self.file,
110+
entity=boundary,
111+
relating_space=space,
112+
related_building_element=wall,
113+
internal_or_external=value,
114+
)
115+
assert boundary.InternalOrExternalBoundary == value

0 commit comments

Comments
 (0)