First Commit

This commit is contained in:
2026-05-31 10:17:09 +07:00
commit 17a9c69379
4547 changed files with 1170384 additions and 0 deletions
@@ -0,0 +1,47 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 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/>.
"""Classification systems are a way of categorising objects
Although IFC itself comes with a built-in classification hierarchy (e.g.
IfcWall and its predefined types of PARTITIONING, etc), there are many external
or custom classification systems such as Uniclass, Omniclass and more. IFC is
able to integrate with any external classification system.
This API allows you to manage and assign external classification systems and
references.
"""
from .. import wrap_usecases
from .add_classification import add_classification
from .add_reference import add_reference
from .edit_classification import edit_classification
from .edit_reference import edit_reference
from .remove_classification import remove_classification
from .remove_reference import remove_reference
wrap_usecases(__path__, __name__)
__all__ = [
"add_classification",
"add_reference",
"edit_classification",
"edit_reference",
"remove_classification",
"remove_reference",
]
@@ -0,0 +1,126 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021, 2022 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 typing import Union
import ifcopenshell
import ifcopenshell.guid
import ifcopenshell.util.date
import ifcopenshell.util.schema
def add_classification(
file: ifcopenshell.file, classification: Union[str, ifcopenshell.entity_instance]
) -> ifcopenshell.entity_instance:
"""Adds a new classification system to the project
External classification systems such as Uniclass or Omniclass are
ways of categorising elements in the AEC industry, typically
standardised or nominated by governments or companies. A system
typically contains a series of hierarchical reference codes and labels
like Pr_12_23_34.
Classifications may be applied to many things, not just physical
elements, such as doors and windows, spatial elements, tasks, cost
items, or even resources.
Prior to assigning classificaion references, you need to add the name
and metadata of the classification system that you will use in your
project. Classification systems may be revised over time, so this
metadata includes the edition date.
Common classification systems are provided as an IFC library which may
be downloaded from https://github.com/Moult/IfcClassification for your
convenience. It is advised to use these to ensure that the
classification metadata is standardised.
Adding a classification system will not add the entire hierarchy of
references available in the classification. References need to be added
separately. Typically, you'd only add the references that you use in
your project, see ifcopenshell.api.classification.add_reference for more
information.
:param classification: If a string is provided, it is assumed to be the
name of your classification system. This is necessary if you are
creating your own custom classification system. Alternatively, you
may provide an entity_instance of an IfcClassification from an IFC
classification library. The latter approach is preferred if you are
using a commonly known system such as Uniclass, as this will ensure
all metadata is added correctly.
:return: The added IfcClassification element
Example:
.. code:: python
# Option 1: adding a custom clasification from scratch
ifcopenshell.api.classification.add_classification(model,
classification="MyCustomClassification")
# Option 2: adding a popular classification from a library
library = ifcopenshell.open("/path/to/Uniclass.ifc")
classification = library.by_type("IfcClassification")[0]
ifcopenshell.api.classification.add_classification(model,
classification=classification)
"""
usecase = Usecase()
usecase.file = file
return usecase.execute(classification)
class Usecase:
file: ifcopenshell.file
def execute(self, classification: Union[str, ifcopenshell.entity_instance]) -> ifcopenshell.entity_instance:
self.classification = classification
if isinstance(self.classification, str):
classification = self.file.create_entity("IfcClassification", Name=self.classification)
self.relate_to_project(classification)
return classification
return self.add_from_library()
def add_from_library(self) -> ifcopenshell.entity_instance:
edition_date = None
if self.classification.EditionDate:
edition_date = ifcopenshell.util.date.ifc2datetime(self.classification.EditionDate)
self.classification.EditionDate = None
migrator = ifcopenshell.util.schema.Migrator()
result = migrator.migrate(self.classification, self.file)
# TODO: should auto date migration be part of the migrator?
if self.file.schema == "IFC2X3" and edition_date:
result.EditionDate = self.file.create_entity(
"IfcCalendarDate", **ifcopenshell.util.date.datetime2ifc(edition_date, "IfcCalendarDate")
)
else:
if edition_date:
edition_date = ifcopenshell.util.date.datetime2ifc(edition_date, "IfcDate")
result.EditionDate = edition_date
self.relate_to_project(result)
return result
def relate_to_project(self, classification: ifcopenshell.entity_instance) -> None:
self.file.create_entity(
"IfcRelAssociatesClassification",
GlobalId=ifcopenshell.guid.new(),
RelatedObjects=[self.file.by_type("IfcProject")[0]],
RelatingClassification=classification,
)
@@ -0,0 +1,256 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 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 typing import Any, Optional, Union
import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.schema
def add_reference(
file: ifcopenshell.file,
products: list[ifcopenshell.entity_instance],
reference: Optional[ifcopenshell.entity_instance] = None,
identification: Optional[str] = None,
name: Optional[str] = None,
classification: Optional[ifcopenshell.entity_instance] = None,
is_lightweight=True,
) -> Union[ifcopenshell.entity_instance, None]:
"""Adds a new classification reference and assigns it to the list of products
A classification reference is a single entry such as "Pr_12_23_34" that
is part of an external classification system (such as Uniclass or
Omniclass).
References can be added to almost any object in IFC, including physical
objects, object types, properties, tasks, costs, resources, or even
resources such as profiles, documents, libraries, and so on.
Classification references can be added in two ways. Option 1) specify a
custom arbitrary reference, where you have to manually specify the
identification (e.g. "Pr_12_23_45") and name (e.g. "Door Products").
Option 2) add a reference from an IFC classification library. The latter
is preferred if you are using a common classification system such as
Uniclass, as the library will be prepopulated with all the valid
classifications already.
Objects are allowed to have multiple classification references from
multiple classification systems. This means that adding a new reference
will not remove existing references.
References can be inherited from types. This means that if an
IfcWallType has a classification reference of Pr_12_23_34, then all
IfcWall occurrences of that type automatically get the same
classification of Pr_12_23_34. This means that it is more efficient to
assign to types where possible. If a classification reference is
assigned to both the type and an occurrence, then the assignment at the
occurrence will override the type classification.
:param product: The list of IFC objects, properties, or resources you want to
associate the classification reference to.
:param reference: The classification reference entity taken from an
IFC classification library. If you supply this parameter, you will
use option 2.
:param identification: If you choose option 1 and do not specify a
reference, you may manually specify an identification code. The code
is typically a short identifier and may have punctuation to separate
the levels of hierarchy in the classificaion (e.g. Pr_12_23_34).
:param name: If you choose option 1 and do not specify a reference, you
may manually specify a name. The name is typically human readable.
:param classification: The IfcClassification entity in your IFC model
(not the library, if you are doing option 2) that the reference is
part of.
:param is_lightweight: If you are doing option 2, choose whether or not
to only add that particular reference (lighweight) or also add all
of its parent references in the classification hierarchy (not
lighweight). For example, adding a lightweight reference to
Pr_12_23_34 will only add Pr_12_23_34, but adding a heavy reference
to Pr_12_23_34 will also add Pr_12_23 and Pr_12. These parent
references merely help describe the "tree" of classifications, but
is generally unnecessary. Using lightweight classifications are
recommended and is the default.
:raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements.
:return: The newly added IfcClassificationReference
or `None` if `products` was empty list.
Example:
.. code:: python
# Option 1: adding and assigning a new reference from scratch
wall_type = model.by_type("IfcWallType")[0]
classification = ifcopenshell.api.classification.add_classification(
model, classification="MyCustomClassification")
ifcopenshell.api.classification.add_reference(model,
products=[wall_type], classification=classification,
identification="W_01", name="Interior Walls")
# Option 2: adding a popular classification from a library
library = ifcopenshell.open("/path/to/Uniclass.ifc")
lib_classification = library.by_type("IfcClassification")[0]
classification = ifcopenshell.api.classification.add_classification(
model, classification=lib_classification)
reference = [r for r in library.by_type("IfcClassificationReference")
if r.Identification == "XYZ"][0]
ifcopenshell.api.classification.add_reference(model,
products=[wall_type], classification=classification,
reference=reference)
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"products": products,
"reference": reference,
"identification": identification,
"name": name,
"classification": classification,
"is_lightweight": is_lightweight,
}
return usecase.execute()
class Usecase:
file: ifcopenshell.file
settings: dict[str, Any]
def execute(self):
if not self.settings["products"]:
return
if self.settings["reference"]:
referenced = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"])
if set(self.settings["products"]).issubset(referenced):
# nothing to do, all elements already have this reference assigned
return self.settings["reference"]
self.rooted_products: set[ifcopenshell.entity_instance] = set()
self.non_rooted_products: set[ifcopenshell.entity_instance] = set()
for product in self.settings["products"]:
if product.is_a("IfcRoot"):
self.rooted_products.add(product)
else:
self.non_rooted_products.add(product)
if self.non_rooted_products and self.file.schema == "IFC2X3":
raise TypeError(f"Cannot add reference to non-IfcRoot element in IFC2X3: {self.non_rooted_products}.")
if self.settings["reference"]:
return self.add_from_library()
return self.add_from_identification()
def add_from_identification(self):
reference = self.get_existing_reference(self.settings["identification"])
if not reference:
reference = self.file.createIfcClassificationReference(
Name=self.settings["name"], ReferencedSource=self.settings["classification"]
)
if self.file.schema == "IFC2X3":
reference.ItemReference = self.settings["identification"]
else:
reference.Identification = self.settings["identification"]
self.update_relationships(reference)
return reference
def add_from_library(self) -> ifcopenshell.entity_instance:
if hasattr(self.settings["reference"], "ItemReference"):
identification = self.settings["reference"].ItemReference # IFC2X3
else:
identification = self.settings["reference"].Identification
reference = self.get_existing_reference(identification)
if not reference:
migrator = ifcopenshell.util.schema.Migrator()
if self.settings["is_lightweight"]:
old_referenced_source = self.settings["reference"].ReferencedSource
self.settings["reference"].ReferencedSource = None
else:
classification_name = self.settings["classification"].Name
existing_classification = [
c for c in self.file.by_type("IfcClassification") if c.Name == classification_name
]
reference = migrator.migrate(self.settings["reference"], self.file)
if self.settings["is_lightweight"]:
reference.ReferencedSource = self.settings["classification"]
self.settings["reference"].ReferencedSource = old_referenced_source
elif existing_classification:
to_delete = set()
for traversed_reference in self.file.traverse(reference):
if traversed_reference.ReferencedSource.is_a("IfcClassification"):
to_delete.add(traversed_reference.ReferencedSource)
traversed_reference.ReferencedSource = existing_classification[0]
break
for element in to_delete:
self.file.remove(element)
self.update_relationships(reference)
return reference
def get_existing_reference(self, identification: Optional[str] = None) -> Union[ifcopenshell.entity_instance, None]:
for reference in self.file.by_type("IfcClassificationReference"):
if self.file.schema == "IFC2X3":
if reference.ItemReference == identification:
return reference
else:
if reference.Identification == identification:
return reference
def update_relationships(self, reference: ifcopenshell.entity_instance) -> None:
root_rel, non_root_rel = None, None
if self.rooted_products:
if self.file.schema == "IFC2X3":
for rel in self.file.by_type("IfcRelAssociatesClassification"):
if rel.RelatingClassification == reference:
root_rel = rel
break
else:
root_rel = next(iter(reference.ClassificationRefForObjects), None)
if root_rel:
related_objects = set(root_rel.RelatedObjects) | self.rooted_products
root_rel.RelatedObjects = list(related_objects)
ifcopenshell.api.owner.update_owner_history(self.file, element=root_rel)
else:
self.file.create_entity(
"IfcRelAssociatesClassification",
OwnerHistory=ifcopenshell.api.owner.create_owner_history(self.file),
GlobalId=ifcopenshell.guid.new(),
RelatedObjects=list(self.rooted_products),
RelatingClassification=reference,
)
if self.non_rooted_products:
# NOTE: Only Ifc4+. Ifc2x3 is already handled by raising TypeError
non_root_rel = next(iter(reference.ExternalReferenceForResources), None)
if non_root_rel:
related_objects = set(non_root_rel.RelatedResourceObjects) | self.non_rooted_products
non_root_rel.RelatedResourceObjects = list(related_objects)
else:
self.file.create_entity(
"IfcExternalReferenceRelationship",
RelatingReference=reference,
RelatedResourceObjects=list(self.non_rooted_products),
)
@@ -0,0 +1,45 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 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 typing import Any
import ifcopenshell
def edit_classification(
file: ifcopenshell.file, classification: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcClassification
For more information about the attributes and data types of an
IfcClassification, consult the IFC documentation.
:param classification: The IfcClassification entity you want to edit
:param attributes: a dictionary of attribute names and values.
:return: None
Example:
.. code:: python
classification = model.by_type("IfcClassification")[0]
# Change the name of the classification system to "Foo"
ifcopenshell.api.classification.edit_classification(model,
classification=classification, attributes={"Name": "Foo"})
"""
for name, value in attributes.items():
setattr(classification, name, value)
@@ -0,0 +1,45 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 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 typing import Any
import ifcopenshell
def edit_reference(
file: ifcopenshell.file, reference: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcClassificationReference
For more information about the attributes and data types of an
IfcClassificationReference, consult the IFC documentation.
:param reference: The IfcClassificationReference entity you want to edit
:param attributes: a dictionary of attribute names and values.
:return: None
Example:
.. code:: python
reference = model.by_type("IfcClassification")[0]
# Change the name of the reference to "Foo"
ifcopenshell.api.classification.edit_reference(model,
reference=reference, attributes={"Name": "Foo"})
"""
for name, value in attributes.items():
setattr(reference, name, value)
@@ -0,0 +1,76 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 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
import ifcopenshell.util.element
def remove_classification(file: ifcopenshell.file, classification: ifcopenshell.entity_instance) -> None:
"""Removes an IfcClassification from the project and all references
The classification and all of its relationships, children references,
and relationships between objects and child references are completely
removed from a project.
:param classification: The IfcClassification entity you want to remove
:return: None
Example:
.. code:: python
classification = model.by_type("IfcClassification")[0]
ifcopenshell.api.classification.remove_classification(model,
classification=classification)
"""
usecase = Usecase()
usecase.file = file
return usecase.execute(classification)
class Usecase:
file: ifcopenshell.file
def execute(self, classification: ifcopenshell.entity_instance) -> None:
references = self.get_references(classification)
for reference in references:
self.file.remove(reference)
self.file.remove(classification)
for rel in self.file.by_type("IfcRelAssociatesClassification"):
if not rel.RelatingClassification:
history = rel.OwnerHistory
self.file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
if self.file.schema != "IFC2X3":
for rel in self.file.by_type("IfcExternalReferenceRelationship"):
if not rel.RelatingReference:
self.file.remove(rel)
def get_references(self, classification: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
results = []
if self.file.schema == "IFC2X3":
for reference in self.file.by_type("IfcClassificationReference"):
if reference.ReferencedSource == classification:
results.append(reference)
else:
for reference in classification.HasReferences:
results.append(reference)
results.extend(self.get_references(reference))
return results
@@ -0,0 +1,117 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 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
import ifcopenshell.api.owner
import ifcopenshell.util.element
def remove_reference(
file: ifcopenshell.file,
reference: ifcopenshell.entity_instance,
products: list[ifcopenshell.entity_instance],
) -> None:
"""Removes a classification reference from the list of products
If the classification reference is no longer associated to any products,
the classification reference itself is also removed.
:param reference: The IfcClassificationReference entity of the
relationship you want to remove.
:param product: The list fo object entities of the relationship you want to
remove.
:raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements.
:return: None
Example:
.. code:: python
wall_type = model.by_type("IfcWallType")[0]
classification = ifcopenshell.api.classification.add_classification(
model, classification="MyCustomClassification")
reference = ifcopenshell.api.classification.add_reference(model,
products=[wall_type], classification=classification,
identification="W_01", name="Interior Walls")
ifcopenshell.api.classification.remove_reference(model,
reference=reference, products=[wall_type])
"""
is_ifc2x3 = file.schema == "IFC2X3"
products_set = set(products)
referenced = ifcopenshell.util.element.get_referenced_elements(reference)
products_set -= products_set.difference(referenced)
# all products are already unassigned from a reference
if not products_set:
return
rooted_products: set[ifcopenshell.entity_instance] = set()
non_rooted_products: set[ifcopenshell.entity_instance] = set()
for product in products:
if product.is_a("IfcRoot"):
rooted_products.add(product)
else:
non_rooted_products.add(product)
if non_rooted_products and is_ifc2x3:
raise TypeError(f"Cannot add reference to non-IfcRoot element in IFC2X3: {non_rooted_products}.")
if rooted_products:
reference_rels: set[ifcopenshell.entity_instance] = set()
for product in rooted_products:
reference_rels.update(product.HasAssociations)
reference_rels = {
rel
for rel in reference_rels
if rel.is_a("IfcRelAssociatesClassification") and rel.RelatingClassification == reference
}
for rel in reference_rels:
related_objects = set(rel.RelatedObjects) - rooted_products
if related_objects:
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.owner.update_owner_history(file, element=rel)
else:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
if non_rooted_products:
reference_rels: set[ifcopenshell.entity_instance] = set()
for product in non_rooted_products:
rels = getattr(product, "HasExternalReferences", None)
if rels is None:
rels = getattr(product, "HasExternalReference", [])
reference_rels.update(rels)
reference_rels = {rel for rel in reference_rels if rel.RelatingReference == reference}
for rel in reference_rels:
related_objects = set(rel.RelatedResourceObjects) - non_rooted_products
if related_objects:
rel.RelatedResourceObjects = list(related_objects)
else:
file.remove(rel)
# TODO: we only handle lightweight classifications here
referenced_elements = ifcopenshell.util.element.get_referenced_elements(reference)
if not referenced_elements:
file.remove(reference)