Merge remote-tracking branch 'origin/v0.8.0' into datamodel-v1.0

This commit is contained in:
Thomas Krijnen
2026-04-18 20:15:28 +02:00
1655 changed files with 105534 additions and 46934 deletions
@@ -17,20 +17,20 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from collections import deque
from collections.abc import Callable
from functools import partial
from typing import Any, Literal, Optional, Union, get_args
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.api.geometry
import ifcopenshell.api.type
import ifcopenshell.api.project
import ifcopenshell.api.context
import ifcopenshell.api.geometry
import ifcopenshell.api.owner.settings
import ifcopenshell.api.project
import ifcopenshell.api.type
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.element
import ifcopenshell.util.geolocation
import ifcopenshell.util.placement
import ifcopenshell.util.unit
from typing import Optional, Any, Union, Literal, get_args, Callable
from functools import partial
APPENDABLE_ASSET = Literal[
"IfcTypeProduct",
@@ -40,7 +40,7 @@ APPENDABLE_ASSET = Literal[
"IfcProfileDef",
"IfcPresentationStyle",
]
APPENDABLE_ASSET_TYPES = get_args(APPENDABLE_ASSET)
APPENDABLE_ASSET_TYPES: tuple[APPENDABLE_ASSET, ...] = get_args(APPENDABLE_ASSET)
MATERIAL_SETS = ("IfcMaterialLayerSet", "IfcMaterialConstituentSet", "IfcMaterialProfileSet")
@@ -243,6 +243,59 @@ class Usecase:
except RuntimeError:
return None
def material_sets_are_equal(self, set1: ifcopenshell.entity_instance, set2: ifcopenshell.entity_instance) -> bool:
"""Check if two material sets are structurally equivalent."""
if set1.is_a() != set2.is_a():
return False
ifc_class = set1.is_a()
if ifc_class == "IfcMaterialLayerSet":
layers1 = set1.MaterialLayers or []
layers2 = set2.MaterialLayers or []
if len(layers1) != len(layers2):
return False
for l1, l2 in zip(layers1, layers2):
if (l1.Material is None) != (l2.Material is None):
return False
if l1.Material and l1.Material.Name != l2.Material.Name:
return False
if l1.LayerThickness != l2.LayerThickness:
return False
elif ifc_class == "IfcMaterialConstituentSet":
constituents1 = set1.MaterialConstituents or []
constituents2 = set2.MaterialConstituents or []
if len(constituents1) != len(constituents2):
return False
for c1, c2 in zip(constituents1, constituents2):
if (c1.Material is None) != (c2.Material is None):
return False
if c1.Material and c1.Material.Name != c2.Material.Name:
return False
if c1.Name != c2.Name:
return False
elif ifc_class == "IfcMaterialProfileSet":
profiles1 = set1.MaterialProfiles or []
profiles2 = set2.MaterialProfiles or []
if len(profiles1) != len(profiles2):
return False
for p1, p2 in zip(profiles1, profiles2):
if (p1.Material is None) != (p2.Material is None):
return False
if p1.Material and p1.Material.Name != p2.Material.Name:
return False
if (p1.Profile is None) != (p2.Profile is None):
return False
if p1.Profile:
profile_name1 = getattr(p1.Profile, "ProfileName", None)
profile_name2 = getattr(p2.Profile, "ProfileName", None)
if profile_name1 != profile_name2:
return False
return True
def get_existing_element(self, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
"""Get existing element for a library element.
@@ -264,13 +317,17 @@ class Usecase:
name = element.Name
return next((e for e in self.file.by_type("IfcMaterial") if e.Name == name), None)
elif element in MATERIAL_SETS:
elif element.is_a() in MATERIAL_SETS:
ifc_class = element.is_a()
name_attr = "LayerSetName" if ifc_class == "IfcMaterialLayerSet" else "Name"
material_set_name = getattr(element, name_attr)
if material_set_name is None:
return
return next((e for e in self.file.by_type(ifc_class) if getattr(e, name_attr) == material_set_name), None)
for candidate in self.file.by_type(ifc_class):
if getattr(candidate, name_attr) == material_set_name:
if self.material_sets_are_equal(element, candidate):
return candidate
return None
elif element.is_a("IfcProfileDef"):
profile_name = element.ProfileName
@@ -324,6 +381,7 @@ class Usecase:
def append_type_product(self):
self.whitelisted_inverse_attributes = {
"IfcObjectDefinition": ["HasAssociations"],
"IfcDistributionElementType": ["IsNestedBy"],
self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"],
"IfcRepresentationItem": ["StyledByItem", "LayerAssignment"],
"IfcRepresentation": ["LayerAssignments"],
@@ -340,6 +398,7 @@ class Usecase:
"IfcObjectDefinition": ["HasAssociations"],
"IfcObject": ["IsDefinedBy.IfcRelDefinesByProperties"],
"IfcElement": ["HasOpenings"],
"IfcDistributionElement": ["IsNestedBy"],
self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"],
"IfcRepresentationItem": [
"StyledByItem",
@@ -372,7 +431,7 @@ class Usecase:
)
ifcopenshell.api.type.assign_type(
self.file,
should_run_listeners=False,
should_run_listeners=False, # ty:ignore[unknown-argument]
related_objects=[element],
relating_type=new_type,
should_map_representations=False,
@@ -512,6 +571,8 @@ class Usecase:
return False
elif element.is_a("IfcRoot") and self.by_guid(element.GlobalId) is not None:
return False
elif element.is_a("IfcDistributionPort"):
return False
elif element.is_a(self.target_class):
return True
elif self.target_class == "IfcProduct" and element.is_a("IfcTypeProduct"):
@@ -665,12 +726,11 @@ class Usecase:
name_attr = "LayerSetName" if ifc_class == "IfcMaterialLayerSet" else "Name"
material_set_name = getattr(element, name_attr)
if material_set_name is not None:
existing_material_set = next(
(e for e in ifc_file.by_type(ifc_class) if getattr(e, name_attr) == material_set_name), None
)
if existing_material_set is not None:
reuse_identities[element_identity] = existing_material_set
return existing_material_set
for candidate in ifc_file.by_type(ifc_class):
if getattr(candidate, name_attr) == material_set_name:
if self.material_sets_are_equal(element, candidate):
reuse_identities[element_identity] = candidate
return candidate
elif element.is_a("IfcPresentationStyle"):
style_name = element.Name
@@ -16,11 +16,12 @@
# 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 typing import Union
import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.guid
import ifcopenshell.util.element
from typing import Union
def assign_declaration(
@@ -17,6 +17,7 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import datetime
import ifcopenshell
import ifcopenshell.util.schema
@@ -52,9 +53,7 @@ def create_file(version: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4") -> ifcope
"""
file = ifcopenshell.file(schema=version)
file.header.file_name.name = "/dev/null" # Hehehe
file.header.file_name.time_stamp = (
datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).astimezone().replace(microsecond=0).isoformat()
)
file.header.file_name.time_stamp = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
file.header.file_name.originating_system = "IfcOpenShell {}".format(ifcopenshell.version)
file.header.file_name.authorization = "Nobody"