diff --git a/src/bonsai/bonsai/bim/module/model/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py
index f50f57bb03..5d147fe520 100644
--- a/src/bonsai/bonsai/bim/module/model/opening.py
+++ b/src/bonsai/bonsai/bim/module/model/opening.py
@@ -26,7 +26,7 @@ import numpy as np
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.geometry
-import ifcopenshell.api.void
+import ifcopenshell.api.feature
import ifcopenshell.geom
import ifcopenshell.util.shape
import ifcopenshell.util.element
@@ -69,7 +69,7 @@ class FilledOpeningGenerator:
assert filling and element
if filling.FillsVoids:
ifcopenshell.api.run(
- "void.remove_opening", tool.Ifc.get(), opening=filling.FillsVoids[0].RelatingOpeningElement
+ "feature.remove_feature", tool.Ifc.get(), feature=filling.FillsVoids[0].RelatingOpeningElement
)
if target is None:
@@ -156,8 +156,8 @@ class FilledOpeningGenerator:
"geometry.assign_representation", tool.Ifc.get(), product=opening, representation=mapped_representation
)
- ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=opening, element=element)
- ifcopenshell.api.run("void.add_filling", tool.Ifc.get(), opening=opening, element=filling)
+ ifcopenshell.api.run("feature.add_feature", tool.Ifc.get(), feature=opening, element=element)
+ ifcopenshell.api.run("feature.add_filling", tool.Ifc.get(), opening=opening, element=filling)
voided_objs = [voided_obj]
# Openings affect all subelements of an aggregate
@@ -826,7 +826,7 @@ class CloneOpening(Operator, tool.Ifc.Operator):
new_opening = ifcopenshell.api.run("root.create_entity", tool.Ifc.get(), ifc_class="IfcOpeningElement")
new_opening.Representation = opening_representation
- ifcopenshell.api.void.add_opening(ifc_file, opening=new_opening, element=voided_element)
+ ifcopenshell.api.feature.add_feature(ifc_file, feature=new_opening, element=voided_element)
new_opening.ObjectPlacement = opening_placement
# Update affected representations.
diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py
index b848a23f5d..8f776a0b36 100644
--- a/src/bonsai/bonsai/bim/module/model/wall.py
+++ b/src/bonsai/bonsai/bim/module/model/wall.py
@@ -984,7 +984,7 @@ class DumbWallJoiner:
_, opening_position = mathutils.geometry.intersect_point_line(opening_location.to_2d(), *axis1["reference"])
if opening_position > cut_percentage:
# The opening should be removed from element1.
- ifcopenshell.api.run("void.remove_opening", tool.Ifc.get(), opening=opening)
+ ifcopenshell.api.run("feature.remove_feature", tool.Ifc.get(), feature=opening)
# Now let's check element2.
for opening in [
@@ -996,7 +996,7 @@ class DumbWallJoiner:
_, opening_position = mathutils.geometry.intersect_point_line(opening_location.to_2d(), *axis1["reference"])
if opening_position < cut_percentage:
# The opening should be removed from element2.
- ifcopenshell.api.run("void.remove_opening", tool.Ifc.get(), opening=opening)
+ ifcopenshell.api.run("feature.remove_feature", tool.Ifc.get(), feature=opening)
# During the duplication process, filled voids are not copied. So we
# only need to check fillings on the original element1.
diff --git a/src/bonsai/bonsai/bim/module/void/operator.py b/src/bonsai/bonsai/bim/module/void/operator.py
index e592b58810..067dc91332 100644
--- a/src/bonsai/bonsai/bim/module/void/operator.py
+++ b/src/bonsai/bonsai/bim/module/void/operator.py
@@ -87,7 +87,7 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
self.report({"INFO"}, f"An {element1.is_a()} is not allowed to have an opening.")
continue
- # Sync placement before void.add_opening.
+ # Sync placement before feature.add_feature.
if tool.Ifc.is_moved(obj1):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj1)
@@ -109,7 +109,7 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
should_add_representation=True,
context=body_context,
)
- ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=element2, element=element1)
+ ifcopenshell.api.run("feature.add_feature", tool.Ifc.get(), feature=element2, element=element1)
if tool.Ifc.is_moved(obj2):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj2)
@@ -178,7 +178,7 @@ class RemoveOpening(bpy.types.Operator, tool.Ifc.Operator):
opening_obj.name = "/".join(opening_obj.name.split("/")[1:])
tool.Ifc.unlink(element=opening)
- ifcopenshell.api.run("void.remove_opening", tool.Ifc.get(), opening=opening)
+ ifcopenshell.api.run("feature.remove_feature", tool.Ifc.get(), feature=opening)
decomposed_building_elements = {element}
decomposed_building_elements.update(tool.Aggregate.get_parts_recursively(element))
@@ -220,7 +220,7 @@ class AddFilling(bpy.types.Operator, tool.Ifc.Operator):
if not element_id or not opening_id or element_id == opening_id:
return {"FINISHED"}
ifcopenshell.api.run(
- "void.add_filling", self.file, opening=self.file.by_id(opening_id), element=self.file.by_id(element_id)
+ "feature.add_filling", self.file, opening=self.file.by_id(opening_id), element=self.file.by_id(element_id)
)
return {"FINISHED"}
@@ -233,10 +233,9 @@ class RemoveFilling(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
filling = tool.Ifc.get().by_id(self.filling)
- filling_obj = tool.Ifc.get_object(filling)
for rel in filling.FillsVoids:
bpy.ops.bim.remove_opening(opening_id=rel.RelatingOpeningElement.id())
- ifcopenshell.api.run("void.remove_filling", tool.Ifc.get(), element=filling)
+ ifcopenshell.api.run("feature.remove_filling", tool.Ifc.get(), element=filling)
return {"FINISHED"}
diff --git a/src/bonsai/bonsai/tool/feature.py b/src/bonsai/bonsai/tool/feature.py
index d8bcbfb162..e9a4d9f779 100644
--- a/src/bonsai/bonsai/tool/feature.py
+++ b/src/bonsai/bonsai/tool/feature.py
@@ -40,13 +40,13 @@ class Feature(bonsai.core.tool.Feature):
for feature_obj in feature_objs:
feature_element = tool.Ifc.get_entity(feature_obj)
- # Sync placement before void.add_opening.
+ # Sync placement before feature.add_feature.
if tool.Ifc.is_moved(featured_obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=featured_obj)
element_had_openings = tool.Geometry.has_openings(featured_element)
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body")
- ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=feature_element, element=featured_element)
+ ifcopenshell.api.run("feature.add_feature", tool.Ifc.get(), feature=feature_element, element=featured_element)
if tool.Ifc.is_moved(feature_obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=feature_obj)
diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py
index 7b64e173af..7d91716e08 100644
--- a/src/bonsai/bonsai/tool/root.py
+++ b/src/bonsai/bonsai/tool/root.py
@@ -290,8 +290,8 @@ class Root(bonsai.core.tool.Root):
product=opening,
representation=mapped_representation,
)
- ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=opening, element=element)
- ifcopenshell.api.run("void.add_filling", tool.Ifc.get(), opening=opening, element=filling)
+ ifcopenshell.api.run("feature.add_feature", tool.Ifc.get(), feature=opening, element=element)
+ ifcopenshell.api.run("feature.add_filling", tool.Ifc.get(), opening=opening, element=filling)
voided_objs = [voided_obj]
# Openings affect all subelements of an aggregate
diff --git a/src/bonsai/test/tool/test_root.py b/src/bonsai/test/tool/test_root.py
index 79b773d256..0d0e1fdce2 100644
--- a/src/bonsai/test/tool/test_root.py
+++ b/src/bonsai/test/tool/test_root.py
@@ -74,8 +74,8 @@ class TestGetDecompositionRelationships(NewFile):
element = ifc.createIfcWall()
opening = ifc.createIfcOpeningElement()
fill = ifc.createIfcWindow()
- ifcopenshell.api.run("void.add_opening", ifc, opening=opening, element=element)
- ifcopenshell.api.run("void.add_filling", ifc, opening=opening, element=fill)
+ ifcopenshell.api.run("feature.add_feature", ifc, feature=opening, element=element)
+ ifcopenshell.api.run("feature.add_filling", ifc, opening=opening, element=fill)
obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(fill, obj)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/void/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/feature/__init__.py
similarity index 71%
rename from src/ifcopenshell-python/ifcopenshell/api/void/__init__.py
rename to src/ifcopenshell-python/ifcopenshell/api/feature/__init__.py
index 0c5306e46f..41c7c27a6c 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/void/__init__.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/feature/__init__.py
@@ -16,25 +16,26 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see .
-"""Create void relationships between openings and physical elements
+"""Create relationships between features (e.g. openings) and physical elements
-An opening is a special element (created using
+A feature is a special element (created using
:func:`ifcopenshell.api.root.create_entity`) that may then be used to create
-voids in other elements (such as walls and slabs). These voids may then be
-filled with doors, trapdoors, skylights, and so on.
+geometric changes in other elements (such as walls and slabs). Most commonly, a
+feature would be an opening void. These voids may then be filled with doors,
+trapdoors, skylights, and so on.
"""
from .. import wrap_usecases
+from .add_feature import add_feature
from .add_filling import add_filling
-from .add_opening import add_opening
+from .remove_feature import remove_feature
from .remove_filling import remove_filling
-from .remove_opening import remove_opening
wrap_usecases(__path__, __name__)
__all__ = [
+ "add_feature",
"add_filling",
- "add_opening",
+ "remove_feature",
"remove_filling",
- "remove_opening",
]
diff --git a/src/ifcopenshell-python/ifcopenshell/api/feature/add_feature.py b/src/ifcopenshell-python/ifcopenshell/api/feature/add_feature.py
new file mode 100644
index 0000000000..4eb3693d19
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/api/feature/add_feature.py
@@ -0,0 +1,152 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2021 Dion Moult
+#
+# 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 .
+
+import ifcopenshell
+import ifcopenshell.api.owner
+import ifcopenshell.api.geometry
+import ifcopenshell.api.aggregate
+import ifcopenshell.guid
+import ifcopenshell.util.element
+import ifcopenshell.util.placement
+
+
+def add_feature(
+ file: ifcopenshell.file, feature: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance
+) -> ifcopenshell.entity_instance:
+ """Create a projecting, voiding, or surface feature in an element
+
+ There are three main types of features: those that add, remove, or
+ influence geometry of a parent object.
+
+ The most common of these is an opening. For example, it is often necessary
+ to cut out openings in elements like walls and slabs to make space to
+ insert doors, windows, and other services that go through these
+ penetrations.
+
+ Whereas it is possible to simply draw the wall as a rectangle with a
+ hole in it for the opening, often these openings have specific meanings.
+ For example, an opening might be filled with a window, and so when the
+ window moves, the opening should move with it. Alternatively, the
+ opening itself might have fire or acoustic requirements, such that any
+ service or equipment passing through that space must also comply with
+ those requirements. For these types of semantic openings, you should
+ have a distinct opening element which voids your regular element. For
+ example, your wall will still be a rectangular prism with no hole in it,
+ and a separate opening element will have a box representing the extents
+ of the opening for a window. The opening element will automatically
+ perform a geometric boolean operation to cut out the wall's geometry.
+
+ Whenever you have an opening in you project, you should determine
+ whether or not the opening is semantic (i.e. should be represented by a
+ distinct opening object) or non-semantic (i.e. should simply be
+ booleaned or be part of the shape of the object).
+
+ :param feature: The IfcFeatureElement to affect the element.
+ :type feature: ifcopenshell.entity_instance
+ :param element: The IfcElement to add the feature to.
+ :type element: ifcopenshell.entity_instance
+ :return: The new IfcRelVoidsElement relationship
+ :rtype: ifcopenshell.entity_instance
+
+ Example:
+
+ .. code:: python
+
+ # A bit of preparation, let's create some geometric contexts since
+ # we want to create some geometry for our wall and opening.
+ model3d = ifcopenshell.api.context.add_context(model, context_type="Model")
+ body = ifcopenshell.api.context.add_context(model,
+ context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model3d)
+
+ # Create a wall
+ wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
+
+ # Let's use the "3D Body" representation we created earlier to add a
+ # new wall-like body geometry, 5 meters long, 3 meters high, and
+ # 200mm thick
+ representation = ifcopenshell.api.geometry.add_wall_representation(model,
+ context=body, length=5, height=3, thickness=0.2)
+ ifcopenshell.api.geometry.assign_representation(model,
+ product=wall, representation=representation)
+
+ # Place our wall at the origin
+ ifcopenshell.api.geometry.edit_object_placement(model, product=wall)
+
+ # Create an opening, such as for a service penetration with fire and
+ # acoustic requirements.
+ opening = ifcopenshell.api.root.create_entity(model, ifc_class="IfcOpeningElement")
+
+ # Let's create an opening representation of a 950mm x 2100mm door.
+ # Notice how the thickness is greater than the wall thickness, this
+ # helps resolve floating point resolution errors in 3D.
+ representation = ifcopenshell.api.geometry.add_wall_representation(model,
+ context=body, length=.95, height=2.1, thickness=0.4)
+ ifcopenshell.api.geometry.assign_representation(model,
+ product=opening, representation=representation)
+
+ # Let's shift our door 1 meter along the wall and 100mm along the
+ # wall, to create a nice overlap for the opening boolean.
+ matrix = np.identity(4)
+ matrix[:,3] = [1, -.1, 0, 0]
+ ifcopenshell.api.geometry.edit_object_placement(model, product=opening, matrix=matrix)
+
+ # The opening will now void the wall.
+ ifcopenshell.api.feature.add_feature(model, feature=opening, element=wall)
+ """
+ if feature.is_a("IfcFeatureElementSubtraction"):
+ rels = feature.VoidsElements
+ ifc_class = "IfcRelVoidsElement"
+ elif feature.is_a("IfcFeatureElementAddition"):
+ rels = feature.ProjectsElements
+ ifc_class = "IfcRelProjectsElement"
+ elif feature.is_a("IfcSurfaceFeature"):
+ if file.schema == "IFC4":
+ return ifcopenshell.api.aggregate.assign_object(file, [feature], element)
+ rels = feature.AdheresToElement
+ ifc_class = "IfcRelAdheresToElement"
+
+ if rels:
+ if rels[0][4] == element:
+ return rels[0]
+ elif ifc_class == "IfcRelAdheresToElement" and len(rels[0].RelatedSurfaceFeatures) != 1:
+ rels[0].RelatedSurfaceFeatures = list(set(rels[0].RelatedSurfaceFeatures) - {feature})
+ else:
+ history = rels[0].OwnerHistory
+ file.remove(rels[0])
+ if history:
+ ifcopenshell.util.element.remove_deep2(file, history)
+
+ rel = file.create_entity(
+ ifc_class,
+ ifcopenshell.guid.new(),
+ ifcopenshell.api.owner.create_owner_history(file),
+ None,
+ None,
+ element,
+ [feature] if ifc_class == "IfcRelAdheresToElement" else feature,
+ )
+
+ if (placement := feature.ObjectPlacement) and placement.is_a("IfcLocalPlacement"):
+ ifcopenshell.api.geometry.edit_object_placement(
+ file,
+ product=feature,
+ matrix=ifcopenshell.util.placement.get_local_placement(placement),
+ is_si=False,
+ )
+
+ return rel
diff --git a/src/ifcopenshell-python/ifcopenshell/api/void/add_filling.py b/src/ifcopenshell-python/ifcopenshell/api/feature/add_filling.py
similarity index 96%
rename from src/ifcopenshell-python/ifcopenshell/api/void/add_filling.py
rename to src/ifcopenshell-python/ifcopenshell/api/feature/add_filling.py
index 0fb7d2e906..7f59d65ec8 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/void/add_filling.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/feature/add_filling.py
@@ -82,7 +82,7 @@ def add_filling(
ifcopenshell.api.geometry.edit_object_placement(model, product=opening, matrix=matrix)
# The opening will now void the wall.
- ifcopenshell.api.void.add_opening(model, opening=opening, element=wall)
+ ifcopenshell.api.feature.add_feature(model, feature=opening, element=wall)
# Create a door
door = ifcopenshell.api.root.create_entity(model, ifc_class="IfcDoor")
@@ -100,7 +100,7 @@ def add_filling(
ifcopenshell.api.geometry.edit_object_placement(model, product=door, matrix=matrix)
# The door will now fill the opening.
- ifcopenshell.api.void.add_filling(model, opening=opening, element=door)
+ ifcopenshell.api.feature.add_filling(model, opening=opening, element=door)
"""
settings = {"opening": opening, "element": element}
diff --git a/src/ifcopenshell-python/ifcopenshell/api/void/remove_opening.py b/src/ifcopenshell-python/ifcopenshell/api/feature/remove_feature.py
similarity index 55%
rename from src/ifcopenshell-python/ifcopenshell/api/void/remove_opening.py
rename to src/ifcopenshell-python/ifcopenshell/api/feature/remove_feature.py
index 21883bcec4..6a1d5e5610 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/void/remove_opening.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/feature/remove_feature.py
@@ -17,44 +17,50 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell.api.root
+import ifcopenshell.api.aggregate
import ifcopenshell.util.element
-def remove_opening(file: ifcopenshell.file, opening: ifcopenshell.entity_instance) -> None:
- """Remove an opening
+def remove_feature(file: ifcopenshell.file, feature: ifcopenshell.entity_instance) -> None:
+ """Remove a feature
- Fillings are retained as orphans. Voided elements remain. Openings
- cannot exist by themselves, so not only is the opening relationship
- removed, the opening is also removed.
+ Fillings are retained as orphans. Featured elements remain. Features
+ cannot exist by themselves, so not only is the relationship removed, the
+ feature is also removed.
- :param opening: The IfcOpeningElement to remove.
- :type opening: ifcopenshell.entity_instance
- :return: None
- :rtype: None
+ :param feature: The IfcFeatureElement to remove.
Example:
.. code:: python
- # Create an oprhaned opening. Note that an orphaned opening is
+ # Create an orphaned opening. Note that an orphaned opening is
# invalid, as an opening can only exist when voiding another
# element.
- opening = ifcopenshell.api.root.create_entity(model, ifc_class="IfcOpeningElement")
+ feature = ifcopenshell.api.root.create_entity(model, ifc_class="IfcOpeningElement")
# Remove it. This brings us back to a valid model.
- ifcopenshell.api.void.remove_opening(model, opening=opening)
+ ifcopenshell.api.feature.remove_feature(model, feature=feature)
"""
- settings = {"opening": opening}
-
- for rel in settings["opening"].VoidsElements:
+ if feature.is_a("IfcFeatureElementSubtraction"):
+ rels = feature.VoidsElements
+ elif feature.is_a("IfcFeatureElementAddition"):
+ rels = feature.ProjectsElements
+ elif feature.is_a("IfcSurfaceFeature"):
+ if file.schema == "IFC4":
+ ifcopenshell.api.aggregate.unassign_object(file, products=[feature])
+ rels = []
+ else:
+ rels = feature.ProjectsElements
+ for rel in rels:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
- if settings["opening"].is_a("IfcOpeningElement"):
- for rel in settings["opening"].HasFillings:
+ if feature.is_a("IfcOpeningElement"):
+ for rel in feature.HasFillings:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
- ifcopenshell.api.root.remove_product(file, product=settings["opening"])
+ ifcopenshell.api.root.remove_product(file, product=feature)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/void/remove_filling.py b/src/ifcopenshell-python/ifcopenshell/api/feature/remove_filling.py
similarity index 93%
rename from src/ifcopenshell-python/ifcopenshell/api/void/remove_filling.py
rename to src/ifcopenshell-python/ifcopenshell/api/feature/remove_filling.py
index af6d28fc6d..f6931b4f9d 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/void/remove_filling.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/feature/remove_filling.py
@@ -47,10 +47,10 @@ def remove_filling(file: ifcopenshell.file, element: ifcopenshell.entity_instanc
door = ifcopenshell.api.root.create_entity(model, ifc_class="IfcDoor")
# The door will now fill the opening.
- ifcopenshell.api.void.add_filling(model, opening=opening, element=door)
+ ifcopenshell.api.feature.add_filling(model, opening=opening, element=door)
# Not anymore!
- ifcopenshell.api.void.remove_filling(model, element=door)
+ ifcopenshell.api.feature.remove_filling(model, element=door)
"""
settings = {"element": element}
diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py
index 77fbc4264d..2df1b4eaa5 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py
@@ -18,7 +18,7 @@
import ifcopenshell.api.type
import ifcopenshell.api.grid
-import ifcopenshell.api.void
+import ifcopenshell.api.feature
import ifcopenshell.api.root
import ifcopenshell.api.pset
import ifcopenshell.api.boundary
@@ -94,7 +94,7 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc
)
ifcopenshell.api.geometry.remove_representation(file, **{"representation": representation})
for opening in getattr(settings["product"], "HasOpenings", []) or []:
- ifcopenshell.api.void.remove_opening(file, opening=opening.RelatedOpeningElement)
+ ifcopenshell.api.feature.remove_feature(file, feature=opening.RelatedOpeningElement)
if settings["product"].is_a("IfcGrid"):
for axis in settings["product"].UAxes + settings["product"].VAxes + (settings["product"].WAxes or ()):
diff --git a/src/ifcopenshell-python/ifcopenshell/api/void/add_opening.py b/src/ifcopenshell-python/ifcopenshell/api/void/add_feature.py
similarity index 69%
rename from src/ifcopenshell-python/ifcopenshell/api/void/add_opening.py
rename to src/ifcopenshell-python/ifcopenshell/api/void/add_feature.py
index 8c739ff0e4..7edc696fae 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/void/add_opening.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/void/add_feature.py
@@ -19,19 +19,24 @@
import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.api.geometry
+import ifcopenshell.api.aggregate
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.placement
-def add_opening(
- file: ifcopenshell.file, opening: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance
+def add_feature(
+ file: ifcopenshell.file, feature: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
- """Create an opening in an element
+ """Create a projecting, voiding, or surface feature in an element
- It is often necessary to cut out openings in elements like walls and
- slabs to make space to insert doors, windows, and other services that go
- through these penetrations.
+ There are three main types of features: those that add, remove, or
+ influence geometry of a parent object.
+
+ The most common of these is an opening. For example, it is often necessary
+ to cut out openings in elements like walls and slabs to make space to
+ insert doors, windows, and other services that go through these
+ penetrations.
Whereas it is possible to simply draw the wall as a rectangle with a
hole in it for the opening, often these openings have specific meanings.
@@ -51,9 +56,9 @@ def add_opening(
distinct opening object) or non-semantic (i.e. should simply be
booleaned or be part of the shape of the object).
- :param opening: The IfcOpeningElement to cut out the element.
- :type opening: ifcopenshell.entity_instance
- :param element: The IfcElement to insert the opening into.
+ :param feature: The IfcFeatureElement to affect the element.
+ :type feature: ifcopenshell.entity_instance
+ :param element: The IfcElement to add the feature to.
:type element: ifcopenshell.entity_instance
:return: The new IfcRelVoidsElement relationship
:rtype: ifcopenshell.entity_instance
@@ -101,36 +106,46 @@ def add_opening(
ifcopenshell.api.geometry.edit_object_placement(model, product=opening, matrix=matrix)
# The opening will now void the wall.
- ifcopenshell.api.void.add_opening(model, opening=opening, element=wall)
+ ifcopenshell.api.void.add_feature(model, feature=opening, element=wall)
"""
- settings = {"opening": opening, "element": element}
+ if feature.is_a("IfcFeatureElementSubtraction"):
+ rels = feature.VoidsElements
+ ifc_class = "IfcRelVoidsElement"
+ elif feature.is_a("IfcFeatureElementAddition"):
+ rels = feature.ProjectsElements
+ ifc_class = "IfcRelProjectsElement"
+ elif feature.is_a("IfcSurfaceFeature"):
+ if file.schema == "IFC4":
+ return ifcopenshell.api.aggregate.assign_object(file, [feature], element)
+ rels = feature.AdheresToElement
+ ifc_class = "IfcRelAdheresToElement"
- voids_elements = settings["opening"].VoidsElements
-
- if voids_elements:
- if voids_elements[0].RelatingBuildingElement == settings["element"]:
- return voids_elements[0]
- history = voids_elements[0].OwnerHistory
- file.remove(voids_elements[0])
- if history:
- ifcopenshell.util.element.remove_deep2(file, history)
+ if rels:
+ if rels[0][4] == element:
+ return rels[0]
+ elif ifc_class == "IfcRelAdheresToElement" and len(rels[0].RelatedSurfaceFeatures) != 1:
+ rels[0].RelatedSurfaceFeatures = list(set(rels[0].RelatedSurfaceFeatures) - {feature})
+ else:
+ history = rels[0].OwnerHistory
+ file.remove(rels[0])
+ if history:
+ ifcopenshell.util.element.remove_deep2(file, history)
rel = file.create_entity(
- "IfcRelVoidsElement",
- **{
- "GlobalId": ifcopenshell.guid.new(),
- "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
- "RelatingBuildingElement": settings["element"],
- "RelatedOpeningElement": settings["opening"],
- }
+ ifc_class,
+ ifcopenshell.guid.new(),
+ ifcopenshell.api.owner.create_owner_history(file),
+ None,
+ None,
+ element,
+ [feature] if ifc_class == "IfcRelAdheresToElement" else feature,
)
- placement = getattr(settings["opening"], "ObjectPlacement", None)
- if placement and placement.is_a("IfcLocalPlacement"):
+ if (placement := feature.ObjectPlacement) and placement.is_a("IfcLocalPlacement"):
ifcopenshell.api.geometry.edit_object_placement(
file,
- product=settings["opening"],
- matrix=ifcopenshell.util.placement.get_local_placement(settings["opening"].ObjectPlacement),
+ product=feature,
+ matrix=ifcopenshell.util.placement.get_local_placement(placement),
is_si=False,
)
diff --git a/src/ifcopenshell-python/test/api/void/__init__.py b/src/ifcopenshell-python/test/api/feature/__init__.py
similarity index 100%
rename from src/ifcopenshell-python/test/api/void/__init__.py
rename to src/ifcopenshell-python/test/api/feature/__init__.py
diff --git a/src/ifcopenshell-python/test/api/void/test_add_opening.py b/src/ifcopenshell-python/test/api/feature/test_add_feature.py
similarity index 63%
rename from src/ifcopenshell-python/test/api/void/test_add_opening.py
rename to src/ifcopenshell-python/test/api/feature/test_add_feature.py
index d1abe2aeea..81ed56ff19 100644
--- a/src/ifcopenshell-python/test/api/void/test_add_opening.py
+++ b/src/ifcopenshell-python/test/api/feature/test_add_feature.py
@@ -18,24 +18,44 @@
import numpy
import test.bootstrap
-import ifcopenshell.api.void
+import ifcopenshell.api.feature
import ifcopenshell.api.root
import ifcopenshell.api.unit
import ifcopenshell.api.geometry
-class TestAddOpening(test.bootstrap.IFC4):
+class TestAddFeature(test.bootstrap.IFC4X3):
+ def test_adding_a_surface_feature(self):
+ wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
+ feature = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcSurfaceFeature")
+ ifcopenshell.api.feature.add_feature(self.file, feature=feature, element=wall)
+ assert wall.HasSurfaceFeatures[0].RelatedSurfaceFeatures == (feature,)
+
+
+class TestAddFeature(test.bootstrap.IFC4):
def test_adding_an_opening(self):
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=wall)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=wall)
assert wall.HasOpenings[0].RelatedOpeningElement == opening
+ def test_adding_a_projection(self):
+ wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
+ projection = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProjectionElement")
+ ifcopenshell.api.feature.add_feature(self.file, feature=projection, element=wall)
+ assert wall.HasProjections[0].RelatedFeatureElement == projection
+
+ def test_adding_a_surface_feature(self):
+ wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
+ feature = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcSurfaceFeature")
+ ifcopenshell.api.feature.add_feature(self.file, feature=feature, element=wall)
+ assert wall.IsDecomposedBy[0].RelatedObjects == (feature,)
+
def test_adding_an_opening_twice(self):
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=wall)
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=wall)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=wall)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=wall)
assert wall.HasOpenings[0].RelatedOpeningElement == opening
assert len(wall.HasOpenings) == 1
@@ -43,8 +63,8 @@ class TestAddOpening(test.bootstrap.IFC4):
slab = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcSlab")
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=slab)
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=wall)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=slab)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=wall)
assert not slab.HasOpenings
assert wall.HasOpenings[0].RelatedOpeningElement == opening
@@ -63,7 +83,7 @@ class TestAddOpening(test.bootstrap.IFC4):
)
ifcopenshell.api.geometry.edit_object_placement(self.file, product=wall, matrix=matrix1.copy(), is_si=False)
ifcopenshell.api.geometry.edit_object_placement(self.file, product=opening, matrix=matrix1.copy(), is_si=False)
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=wall)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=wall)
assert opening.ObjectPlacement.PlacementRelTo.PlacesObject[0] == wall
assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement), matrix1)
@@ -74,9 +94,10 @@ class TestAddOpening(test.bootstrap.IFC4):
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
placement = self.file.createIfcGridPlacement()
opening.ObjectPlacement = placement
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=wall)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=wall)
assert opening.ObjectPlacement == placement
-class TestAddOpeningIFC2X3(test.bootstrap.IFC2X3, TestAddOpening):
- pass
+class TestAddFeatureIFC2X3(test.bootstrap.IFC2X3, TestAddFeature):
+ def test_adding_a_surface_feature(self):
+ pass
diff --git a/src/ifcopenshell-python/test/api/void/test_add_filling.py b/src/ifcopenshell-python/test/api/feature/test_add_filling.py
similarity index 81%
rename from src/ifcopenshell-python/test/api/void/test_add_filling.py
rename to src/ifcopenshell-python/test/api/feature/test_add_filling.py
index 7e0e2edabe..49259d75ab 100644
--- a/src/ifcopenshell-python/test/api/void/test_add_filling.py
+++ b/src/ifcopenshell-python/test/api/feature/test_add_filling.py
@@ -17,7 +17,7 @@
# along with IfcOpenShell. If not, see .
import test.bootstrap
-import ifcopenshell.api.void
+import ifcopenshell.api.feature
import ifcopenshell.api.root
@@ -25,14 +25,14 @@ class TestAddFilling(test.bootstrap.IFC4):
def test_adding_a_filling(self):
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
door = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoor")
- ifcopenshell.api.void.add_filling(self.file, opening=opening, element=door)
+ ifcopenshell.api.feature.add_filling(self.file, opening=opening, element=door)
assert door.FillsVoids[0].RelatingOpeningElement == opening
def test_adding_a_filling_twice(self):
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
door = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoor")
- ifcopenshell.api.void.add_filling(self.file, opening=opening, element=door)
- ifcopenshell.api.void.add_filling(self.file, opening=opening, element=door)
+ ifcopenshell.api.feature.add_filling(self.file, opening=opening, element=door)
+ ifcopenshell.api.feature.add_filling(self.file, opening=opening, element=door)
assert door.FillsVoids[0].RelatingOpeningElement == opening
assert len(opening.HasFillings) == 1
@@ -40,8 +40,8 @@ class TestAddFilling(test.bootstrap.IFC4):
door = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoor")
opening1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
opening2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
- ifcopenshell.api.void.add_filling(self.file, opening=opening1, element=door)
- ifcopenshell.api.void.add_filling(self.file, opening=opening2, element=door)
+ ifcopenshell.api.feature.add_filling(self.file, opening=opening1, element=door)
+ ifcopenshell.api.feature.add_filling(self.file, opening=opening2, element=door)
assert not opening1.HasFillings
assert opening2.HasFillings[0].RelatedBuildingElement == door
diff --git a/src/ifcopenshell-python/test/api/void/test_remove_opening.py b/src/ifcopenshell-python/test/api/feature/test_remove_feature.py
similarity index 71%
rename from src/ifcopenshell-python/test/api/void/test_remove_opening.py
rename to src/ifcopenshell-python/test/api/feature/test_remove_feature.py
index 02beb80b23..5275639525 100644
--- a/src/ifcopenshell-python/test/api/void/test_remove_opening.py
+++ b/src/ifcopenshell-python/test/api/feature/test_remove_feature.py
@@ -17,21 +17,21 @@
# along with IfcOpenShell. If not, see .
import test.bootstrap
-import ifcopenshell.api.void
+import ifcopenshell.api.feature
import ifcopenshell.api.root
-class TestRemoveOpening(test.bootstrap.IFC4):
- def test_removing_a_simple_opening(self):
- opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
- ifcopenshell.api.void.remove_opening(self.file, opening=opening)
+class TestRemoveFeature(test.bootstrap.IFC4):
+ def test_removing_a_simple_feature(self):
+ feature = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
+ ifcopenshell.api.feature.remove_feature(self.file, feature=feature)
assert len(list(self.file)) == 0
def test_removing_an_opening_voiding_a_wall(self):
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=wall)
- ifcopenshell.api.void.remove_opening(self.file, opening=opening)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=wall)
+ ifcopenshell.api.feature.remove_feature(self.file, feature=opening)
assert len(self.file.by_type("IfcOpeningElement")) == 0
assert len(self.file.by_type("IfcRelVoidsElement")) == 0
assert wall
@@ -40,9 +40,9 @@ class TestRemoveOpening(test.bootstrap.IFC4):
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
door = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoor")
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=wall)
- ifcopenshell.api.void.add_filling(self.file, opening=opening, element=door)
- ifcopenshell.api.void.remove_opening(self.file, opening=opening)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=wall)
+ ifcopenshell.api.feature.add_filling(self.file, opening=opening, element=door)
+ ifcopenshell.api.feature.remove_feature(self.file, feature=opening)
assert len(self.file.by_type("IfcOpeningElement")) == 0
assert len(self.file.by_type("IfcRelVoidsElement")) == 0
assert len(self.file.by_type("IfcRelFillsElement")) == 0
@@ -50,5 +50,5 @@ class TestRemoveOpening(test.bootstrap.IFC4):
assert door
-class TestRemoveOpeningIFC2X3(test.bootstrap.IFC2X3, TestRemoveOpening):
+class TestRemoveFeatureIFC2X3(test.bootstrap.IFC2X3, TestRemoveFeature):
pass
diff --git a/src/ifcopenshell-python/test/api/geometry/test_edit_object_placement.py b/src/ifcopenshell-python/test/api/geometry/test_edit_object_placement.py
index 7c6bb46b60..70dcbf7285 100644
--- a/src/ifcopenshell-python/test/api/geometry/test_edit_object_placement.py
+++ b/src/ifcopenshell-python/test/api/geometry/test_edit_object_placement.py
@@ -19,7 +19,7 @@
import numpy
import pytest
import test.bootstrap
-import ifcopenshell.api.void
+import ifcopenshell.api.feature
import ifcopenshell.api.root
import ifcopenshell.api.unit
import ifcopenshell.api.system
@@ -303,7 +303,7 @@ class TestEditObjectPlacement(test.bootstrap.IFC4):
(0.0, 0.0, 0.0, 1.0),
)
)
- ifcopenshell.api.void.add_opening(self.file, opening=subelement, element=element)
+ ifcopenshell.api.feature.add_feature(self.file, feature=subelement, element=element)
ifcopenshell.api.geometry.edit_object_placement(self.file, product=element, matrix=matrix.copy(), is_si=False)
ifcopenshell.api.geometry.edit_object_placement(
self.file, product=subelement, matrix=submatrix.copy(), is_si=False
@@ -337,8 +337,8 @@ class TestEditObjectPlacement(test.bootstrap.IFC4):
(0.0, 0.0, 0.0, 1.0),
)
)
- ifcopenshell.api.void.add_opening(self.file, opening=element, element=wall)
- ifcopenshell.api.void.add_filling(self.file, element=subelement, opening=element)
+ ifcopenshell.api.feature.add_feature(self.file, feature=element, element=wall)
+ ifcopenshell.api.feature.add_filling(self.file, element=subelement, opening=element)
ifcopenshell.api.geometry.edit_object_placement(self.file, product=site, matrix=numpy.eye(4), is_si=False)
ifcopenshell.api.geometry.edit_object_placement(self.file, product=wall, matrix=numpy.eye(4), is_si=False)
ifcopenshell.api.geometry.edit_object_placement(self.file, product=element, matrix=matrix.copy(), is_si=False)
@@ -564,8 +564,8 @@ class TestEditObjectPlacement(test.bootstrap.IFC4):
shifted_submatrix = numpy.eye(4)
shifted_submatrix[:3, 3] = (1, 3, 5)
- ifcopenshell.api.void.add_opening(self.file, opening=subelement, element=element)
- ifcopenshell.api.void.add_filling(self.file, opening=subelement, element=subsubelement)
+ ifcopenshell.api.feature.add_feature(self.file, feature=subelement, element=element)
+ ifcopenshell.api.feature.add_filling(self.file, opening=subelement, element=subsubelement)
previous_placement_id = ifcopenshell.api.geometry.edit_object_placement(
self.file, product=element, matrix=matrix.copy(), is_si=False
).id()
diff --git a/src/ifcopenshell-python/test/api/root/test_copy_class.py b/src/ifcopenshell-python/test/api/root/test_copy_class.py
index 01175eb762..66ba27ec06 100644
--- a/src/ifcopenshell-python/test/api/root/test_copy_class.py
+++ b/src/ifcopenshell-python/test/api/root/test_copy_class.py
@@ -21,7 +21,7 @@ import test.bootstrap
import ifcopenshell.api.unit
import ifcopenshell.api.type
import ifcopenshell.api.root
-import ifcopenshell.api.void
+import ifcopenshell.api.feature
import ifcopenshell.api.pset
import ifcopenshell.api.group
import ifcopenshell.api.system
@@ -134,7 +134,7 @@ class TestCopyClass(test.bootstrap.IFC4):
# IfcOpeningElement opening
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=wall)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=wall)
new = ifcopenshell.api.root.copy_class(self.file, product=wall)
assert wall.HasOpenings[0] != new.HasOpenings[0]
assert wall.HasOpenings[0].RelatedOpeningElement == opening
@@ -144,7 +144,7 @@ class TestCopyClass(test.bootstrap.IFC4):
# IfcVoidingFeature opening
plate = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcPlate")
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcVoidingFeature")
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=plate)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=plate)
new = ifcopenshell.api.root.copy_class(self.file, product=plate)
assert plate.HasOpenings[0] != new.HasOpenings[0]
assert plate.HasOpenings[0].RelatedOpeningElement == opening
@@ -155,15 +155,15 @@ class TestCopyClass(test.bootstrap.IFC4):
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
window = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWindow")
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=wall)
- ifcopenshell.api.void.add_filling(self.file, opening=opening, element=window)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=wall)
+ ifcopenshell.api.feature.add_filling(self.file, opening=opening, element=window)
new = ifcopenshell.api.root.copy_class(self.file, product=wall)
assert not new.HasOpenings
def test_copying_an_opening_voiding_an_element(self):
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=wall)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=wall)
new = ifcopenshell.api.root.copy_class(self.file, product=opening)
assert opening.VoidsElements[0] != new.VoidsElements[0]
assert new.VoidsElements[0].RelatingBuildingElement == wall
@@ -171,14 +171,14 @@ class TestCopyClass(test.bootstrap.IFC4):
def test_copying_an_opening_with_a_filling(self):
door = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoor")
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
- ifcopenshell.api.void.add_filling(self.file, opening=opening, element=door)
+ ifcopenshell.api.feature.add_filling(self.file, opening=opening, element=door)
new = ifcopenshell.api.root.copy_class(self.file, product=opening)
assert not new.HasFillings
def test_copying_a_filling(self):
door = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoor")
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
- ifcopenshell.api.void.add_filling(self.file, opening=opening, element=door)
+ ifcopenshell.api.feature.add_filling(self.file, opening=opening, element=door)
new = ifcopenshell.api.root.copy_class(self.file, product=door)
assert not new.FillsVoids
diff --git a/src/ifcopenshell-python/test/api/root/test_remove_product.py b/src/ifcopenshell-python/test/api/root/test_remove_product.py
index 7b6cef8295..da9b46dbe5 100644
--- a/src/ifcopenshell-python/test/api/root/test_remove_product.py
+++ b/src/ifcopenshell-python/test/api/root/test_remove_product.py
@@ -20,7 +20,7 @@ import test.bootstrap
import ifcopenshell.api.unit
import ifcopenshell.api.nest
import ifcopenshell.api.root
-import ifcopenshell.api.void
+import ifcopenshell.api.feature
import ifcopenshell.api.grid
import ifcopenshell.api.type
import ifcopenshell.api.pset
@@ -159,7 +159,7 @@ class TestRemoveProduct(test.bootstrap.IFC4):
def test_removing_all_openings_of_an_element(self):
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=element)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=element)
ifcopenshell.api.root.remove_product(self.file, product=element)
assert len(list(self.file)) == 0
assert len(self.file.by_type("IfcWall")) == 0
@@ -180,7 +180,7 @@ class TestRemoveProduct(test.bootstrap.IFC4):
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
total_entities = len(list(self.file))
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=element)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=element)
ifcopenshell.api.root.remove_product(self.file, product=opening)
assert len(list(self.file)) == total_entities
assert len(self.file.by_type("IfcOpeningElement")) == 0
@@ -191,8 +191,8 @@ class TestRemoveProduct(test.bootstrap.IFC4):
opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
filling = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoor")
total_entities = len(list(self.file))
- ifcopenshell.api.void.add_opening(self.file, opening=opening, element=element)
- ifcopenshell.api.void.add_filling(self.file, opening=opening, element=filling)
+ ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=element)
+ ifcopenshell.api.feature.add_filling(self.file, opening=opening, element=filling)
ifcopenshell.api.root.remove_product(self.file, product=filling)
assert len(list(self.file)) == total_entities
assert len(self.file.by_type("IfcDoor")) == 0
diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py
index 6ebba1b82a..1ab002f7b0 100644
--- a/src/ifcopenshell-python/test/util/test_element.py
+++ b/src/ifcopenshell-python/test/util/test_element.py
@@ -19,7 +19,7 @@
import ifcopenshell.api.profile
import pytest
import test.bootstrap
-import ifcopenshell.api.void
+import ifcopenshell.api.feature
import ifcopenshell.api.pset
import ifcopenshell.api.root
import ifcopenshell.api.type
@@ -841,8 +841,8 @@ class TestGetDecompositionIFC4(test.bootstrap.IFC4):
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
subelement = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
subsubelement = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWindow")
- ifcopenshell.api.void.add_opening(self.file, element=element, opening=subelement)
- ifcopenshell.api.void.add_filling(self.file, element=subsubelement, opening=subelement)
+ ifcopenshell.api.feature.add_feature(self.file, element=element, feature=subelement)
+ ifcopenshell.api.feature.add_filling(self.file, element=subsubelement, opening=subelement)
results = subject.get_decomposition(element)
assert subelement in results
assert subsubelement in results