First Commit
This commit is contained in:
@@ -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/>.
|
||||
|
||||
"""Property sets and quantity sets let you store simple key value metadata
|
||||
associated with elements
|
||||
|
||||
This is the simplest and most common way to store information about an element.
|
||||
For example, if a door has a fire rating, it is stored as a property.
|
||||
"""
|
||||
|
||||
from .. import wrap_usecases
|
||||
from .add_pset import add_pset
|
||||
from .add_qto import add_qto
|
||||
from .assign_pset import assign_pset
|
||||
from .edit_pset import edit_pset
|
||||
from .edit_qto import edit_qto
|
||||
from .remove_pset import remove_pset
|
||||
from .unassign_pset import unassign_pset
|
||||
from .unshare_pset import unshare_pset
|
||||
|
||||
wrap_usecases(__path__, __name__)
|
||||
|
||||
__all__ = [
|
||||
"add_pset",
|
||||
"add_qto",
|
||||
"assign_pset",
|
||||
"edit_pset",
|
||||
"edit_qto",
|
||||
"remove_pset",
|
||||
"unassign_pset",
|
||||
"unshare_pset",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,172 @@
|
||||
# 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
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.owner
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.guid
|
||||
|
||||
|
||||
def add_pset(
|
||||
file: ifcopenshell.file,
|
||||
product: ifcopenshell.entity_instance,
|
||||
name: str,
|
||||
ifc2x3_subclass: Optional[str] = None,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""Adds a new property set to a product
|
||||
|
||||
Products, such as physical objects or types in IFC may have properties
|
||||
associated with them. These properties are typically simple key value
|
||||
metadata with data types. For example, a wall type may have a property
|
||||
called FireRating with a text value of "2HR". Properties are grouped
|
||||
into property sets, so that related properties are grouped together.
|
||||
|
||||
If a property is assigned to a type, the property is inherited by all
|
||||
occurrences of that type. For example, a wall type with a FireRating
|
||||
property of "2HR" automatically implies that all walls of that wall type
|
||||
also have a FireRating of "2HR". It is not necessary to explictly define
|
||||
the property again for each occurrence. This also means that properties
|
||||
are typically defined on types. If the same property is defined at an
|
||||
occurrence, this overrides the property defined on the type.
|
||||
|
||||
buildingSMART has come up with a long list of standardised properties
|
||||
for the most common properties required internationally. This solves the
|
||||
age-old question of "where do I store my FireRating data for walls"? The
|
||||
answer, in this case, is in the "FireRating" property with an "IfcLabel"
|
||||
data type grouped in the "Pset_WallCommon" property set. It is
|
||||
recommended to view the list of standardised buildingSMART properties
|
||||
and see if any suit your needs first. If none are appropriate, then you
|
||||
are free to create your own custom properties.
|
||||
|
||||
This function adds a blank named property set. One you have a property
|
||||
set you may add properties using ifcopenshell.api.pset.edit_pset.
|
||||
|
||||
See also ifcopenshell.api.pset.add_qto if you want to add quantification
|
||||
data, rather than arbitrary metadata.
|
||||
|
||||
:param product: The IfcObject that you want to assign a property set to.
|
||||
:param name: The name of the property set. Property sets that are
|
||||
standardised by buildingSMART typically have a prefix of "Pset_",
|
||||
like "Pset_WallCommon". If you create your own, you must not use
|
||||
that prefix. It is recommended to use your own prefix tailored to
|
||||
your project, company, or local government requirement.
|
||||
|
||||
In IFC2X3 should be provided as an empty string for profile properties
|
||||
(they all don't have a name property) and all material properties
|
||||
besides IfcExtendedMaterialProperties.
|
||||
:param ifc2x3_subclass: IFC2X3 subclass for material or profile properties.
|
||||
In IFC2X3 IfcProfileProperties and IfcMaterialProperties are abstract
|
||||
so you need one of their subclasses to instantiate them.
|
||||
By default, for profile will be created IfcGeneralProfileProperties
|
||||
and for material - IfcExtendedMaterialProperties.
|
||||
Will have no effect in >=IFC4.
|
||||
|
||||
:raises TypeError: If `product` class doesn't support adding a pset.
|
||||
|
||||
:return: The newly created IfcPropertySet
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Let's imagine we have a new wall type.
|
||||
wall_type = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWallType")
|
||||
|
||||
# Note that this only creates and assigns an empty property set. We
|
||||
# still need to add properties into the property set. Having blank
|
||||
# property sets are invalid.
|
||||
pset = ifcopenshell.api.pset.add_pset(model, product=wall_type, name="Pset_WallCommon")
|
||||
|
||||
# Add a fire rating property standardised by buildingSMART.
|
||||
ifcopenshell.api.pset.edit_pset(model, pset=pset, properties={"FireRating": "2HR"})
|
||||
"""
|
||||
is_ifc2x3 = file.schema == "IFC2X3"
|
||||
|
||||
if product.is_a("IfcObject") or product.is_a("IfcContext"):
|
||||
for rel in product.IsDefinedBy or []:
|
||||
if rel.is_a("IfcRelDefinesByProperties") and rel.RelatingPropertyDefinition.Name == name:
|
||||
return rel.RelatingPropertyDefinition
|
||||
|
||||
pset = file.create_entity(
|
||||
"IfcPropertySet",
|
||||
**{
|
||||
"GlobalId": ifcopenshell.guid.new(),
|
||||
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
|
||||
"Name": name,
|
||||
},
|
||||
)
|
||||
ifcopenshell.api.pset.assign_pset(file, [product], pset)
|
||||
return pset
|
||||
|
||||
elif product.is_a("IfcTypeObject"):
|
||||
for definition in product.HasPropertySets or []:
|
||||
if definition.Name == name:
|
||||
return definition
|
||||
|
||||
pset = file.create_entity(
|
||||
"IfcPropertySet",
|
||||
**{
|
||||
"GlobalId": ifcopenshell.guid.new(),
|
||||
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
|
||||
"Name": name,
|
||||
},
|
||||
)
|
||||
ifcopenshell.api.pset.assign_pset(file, [product], pset)
|
||||
return pset
|
||||
|
||||
# in IFC2X3 IfcMaterialDefinition not yet existed
|
||||
elif product.is_a("IfcMaterialDefinition") or product.is_a("IfcMaterial"):
|
||||
kwargs: dict[str, Any]
|
||||
kwargs = {"Material": product}
|
||||
if file.schema == "IFC2X3":
|
||||
ifc_class = ifc2x3_subclass or "IfcExtendedMaterialProperties"
|
||||
definitions = (d for d in file.by_type("IfcMaterialProperties") if d.Material == product)
|
||||
if ifc_class == "IfcExtendedMaterialProperties":
|
||||
kwargs["Name"] = name
|
||||
else:
|
||||
ifc_class = "IfcMaterialProperties"
|
||||
definitions = product.HasProperties
|
||||
kwargs["Name"] = name
|
||||
for definition in definitions:
|
||||
# In IFC2X3 not all IfcMaterialProperties has Name
|
||||
if getattr(definition, "Name", None) == name:
|
||||
return definition
|
||||
|
||||
return file.create_entity(ifc_class, **kwargs)
|
||||
|
||||
elif product.is_a("IfcProfileDef"):
|
||||
# in IFC2X3 IfcProfileProperties doesn't have Name and we cannot identify them
|
||||
if file.schema != "IFC2X3":
|
||||
for definition in product.HasProperties or []:
|
||||
if definition.Name == name:
|
||||
return definition
|
||||
|
||||
kwargs = {}
|
||||
kwargs["ProfileDefinition"] = product
|
||||
if file.schema != "IFC2X3":
|
||||
kwargs["Name"] = name
|
||||
|
||||
if is_ifc2x3:
|
||||
ifc_class = ifc2x3_subclass or "IfcGeneralProfileProperties"
|
||||
else:
|
||||
ifc_class = "IfcProfileProperties"
|
||||
return file.create_entity(ifc_class, **kwargs)
|
||||
|
||||
raise TypeError(f"Class '{product.is_a(True)}' doesn't support adding a property set.")
|
||||
@@ -0,0 +1,128 @@
|
||||
# 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
|
||||
import ifcopenshell.api.owner
|
||||
import ifcopenshell.guid
|
||||
|
||||
|
||||
def add_qto(file: ifcopenshell.file, product: ifcopenshell.entity_instance, name: str) -> ifcopenshell.entity_instance:
|
||||
"""Adds a new quantity set to a product
|
||||
|
||||
Products, such as physical objects or types in IFC may have quantities
|
||||
associated with them. These quantities are typically simple key value
|
||||
metadata with data types. For example, a wall type may have a quantity
|
||||
called NetSideArea with a area value of "4.2". Quantities are grouped
|
||||
into quantity sets, so that related quantities are grouped together.
|
||||
|
||||
Quantities are similar to, but different from properties in that they
|
||||
may store a method of measurement or formula. Quantities may also have
|
||||
parametric relationships to other calculated values, such as cost
|
||||
schedules, resource utilisation, or construction task durations.
|
||||
|
||||
buildingSMART has come up with a long list of standardised quantities
|
||||
for the most common quantities required internationally. This solves the
|
||||
age-old question of "what's the standard way of storing quantity
|
||||
take-off data"? It is recommended to view the list of standardised
|
||||
buildingSMART quantities and see if any suit your needs first. If none
|
||||
are appropriate, then you are free to create your own custom quantities.
|
||||
|
||||
This function adds a blank named quantity set. One you have a quantity
|
||||
set you may add quantities using ifcopenshell.api.pset.edit_qto.
|
||||
|
||||
See also ifcopenshell.api.pset.add_qto if you want to arbitrary
|
||||
metadata, rather than quantification data.
|
||||
|
||||
:param product: The IfcObject that you want to assign a quantity set to.
|
||||
:param name: The name of the quantity set. Quantity sets that are
|
||||
standardised by buildingSMART typically have a prefix of "Qto_",
|
||||
like "Qto_WallBaseQuantities". If you create your own, you must not
|
||||
use that prefix. It is recommended to use your own prefix tailored
|
||||
to your project, company, or local government requirement.
|
||||
:return: The newly created IfcElementQuantity
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Let's imagine we have a new wall.
|
||||
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
|
||||
|
||||
# Note that this only creates and assigns an empty quantity set. We
|
||||
# still need to add quantities into the property set. Having blank
|
||||
# quantity sets are invalid.
|
||||
qto = ifcopenshell.api.pset.add_qto(model, product=wall_type, name="Qto_WallBaseQuantities")
|
||||
|
||||
# Add a side area property standardised by buildingSMART. This
|
||||
# allows quantity take-off to occur, even though no geometry has
|
||||
# even been modelled!
|
||||
ifcopenshell.api.pset.edit_qto(model, qto=qto, properties={"NetSideArea": 4.2})
|
||||
"""
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
usecase.settings = {"product": product, "name": name}
|
||||
return usecase.execute()
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
settings: dict[str, Any]
|
||||
|
||||
def execute(self):
|
||||
product: ifcopenshell.entity_instance = self.settings["product"]
|
||||
name: str = self.settings["name"]
|
||||
|
||||
if product.is_a("IfcObject") or product.is_a("IfcContext"):
|
||||
for rel in product.IsDefinedBy or []:
|
||||
if (
|
||||
rel.is_a("IfcRelDefinesByProperties")
|
||||
and rel.RelatingPropertyDefinition.Name == self.settings["name"]
|
||||
):
|
||||
return rel.RelatingPropertyDefinition
|
||||
|
||||
qto = self.create_qto()
|
||||
self.file.create_entity(
|
||||
"IfcRelDefinesByProperties",
|
||||
**{
|
||||
"GlobalId": ifcopenshell.guid.new(),
|
||||
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(self.file),
|
||||
"RelatedObjects": [self.settings["product"]],
|
||||
"RelatingPropertyDefinition": qto,
|
||||
}
|
||||
)
|
||||
return qto
|
||||
elif product.is_a("IfcTypeObject"):
|
||||
for definition in product.HasPropertySets or []:
|
||||
if definition.Name == name:
|
||||
return definition
|
||||
qto = self.create_qto()
|
||||
has_property_sets = list(product.HasPropertySets or [])
|
||||
has_property_sets.append(qto)
|
||||
product.HasPropertySets = has_property_sets
|
||||
return qto
|
||||
|
||||
def create_qto(self):
|
||||
return self.file.create_entity(
|
||||
"IfcElementQuantity",
|
||||
GlobalId=ifcopenshell.guid.new(),
|
||||
OwnerHistory=ifcopenshell.api.owner.create_owner_history(self.file),
|
||||
Name=self.settings["name"],
|
||||
MethodOfMeasurement="BaseQuantities" if self.settings["name"].endswith("BaseQuantities") else None,
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
# 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 Union
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.owner
|
||||
import ifcopenshell.guid
|
||||
|
||||
|
||||
def assign_pset(
|
||||
file: ifcopenshell.file,
|
||||
products: list[ifcopenshell.entity_instance],
|
||||
pset: ifcopenshell.entity_instance,
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Assign property set to provided elements.
|
||||
|
||||
This method can be used to make psets shared by multiple elements.
|
||||
|
||||
:param products: Elements (or element types) to assign the pset to.
|
||||
:param pset: Property set.
|
||||
:return: None if `products` is empty or has only type elements.
|
||||
IfcRelDefinesByProperties if `products` contains occurrences.
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
element = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
|
||||
ifcopenshell.api.pset.assign_pset(model, [element], pset)
|
||||
# Pset is now assigned.
|
||||
assert ifcopenshell.util.element.get_elements_by_pset(pset) == {element}
|
||||
|
||||
element1 = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
|
||||
element2 = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
|
||||
ifcopenshell.api.pset.assign_pset(model, [element1, element2], pset)
|
||||
# Pset is now shared by multiple elements.
|
||||
assert ifcopenshell.util.element.get_elements_by_pset(pset) == {element, element1, element2}
|
||||
|
||||
# Same for element types.
|
||||
element_type = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWallType")
|
||||
ifcopenshell.api.pset.assign_pset(model, [element_type], type_pset)
|
||||
# Pset is now assigned to the type.
|
||||
assert ifcopenshell.util.element.get_elements_by_pset(type_pset) == {element_type}
|
||||
"""
|
||||
is_ifc2x3 = file.schema == "IFC2X3"
|
||||
|
||||
products_occurrences: set[ifcopenshell.entity_instance] = set()
|
||||
products_types: set[ifcopenshell.entity_instance] = set()
|
||||
for product in products:
|
||||
if product.is_a("IfcTypeProduct"):
|
||||
products_types.add(product)
|
||||
else:
|
||||
products_occurrences.add(product)
|
||||
|
||||
rel = None
|
||||
# Check occurrences using pset.
|
||||
if products_occurrences:
|
||||
rels = pset.PropertyDefinitionOf if is_ifc2x3 else pset.DefinesOccurrence
|
||||
rel = next(iter(rels), None)
|
||||
if rel is not None:
|
||||
objs = set(rel.RelatedObjects) | products_occurrences
|
||||
rel.RelatedObjects = list(objs)
|
||||
else:
|
||||
rel = file.create_entity(
|
||||
"IfcRelDefinesByProperties",
|
||||
**{
|
||||
"GlobalId": ifcopenshell.guid.new(),
|
||||
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
|
||||
"RelatedObjects": list(products_occurrences),
|
||||
"RelatingPropertyDefinition": pset,
|
||||
},
|
||||
)
|
||||
|
||||
for product in products_types:
|
||||
psets = list(product.HasPropertySets or [])
|
||||
product.HasPropertySets = psets + [pset]
|
||||
|
||||
return rel
|
||||
@@ -0,0 +1,490 @@
|
||||
# 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 datetime
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.pset
|
||||
|
||||
|
||||
def edit_pset(
|
||||
file: ifcopenshell.file,
|
||||
pset: ifcopenshell.entity_instance,
|
||||
name: Optional[str] = None,
|
||||
properties: Optional[dict[str, Any]] = None,
|
||||
pset_template: Optional[ifcopenshell.entity_instance] = None,
|
||||
should_purge: bool = True,
|
||||
) -> None:
|
||||
"""Edits a property set and its properties
|
||||
|
||||
At its simplest usage, this may be used to edit the name of a property
|
||||
set. It may also be used to add, edit, or remove properties, either
|
||||
arbitrarily or using a property set template.
|
||||
|
||||
A list of properties are provided as a dictionary, where the keys are
|
||||
property names, and values are property values. Keys that don't already
|
||||
exist are interpreted as properties to be added. Keys that already exist
|
||||
are interpreted as properties to be edited. A "None" value may specify a
|
||||
property to be deleted.
|
||||
|
||||
Properties must have a data type. There are lots of data types in IFCs,
|
||||
not just simple unitless data types like integers, booleans, text, but
|
||||
also distinguishing between types of text, like labels versus
|
||||
descriptive text. There are also lots of unit-based data types like
|
||||
areas, volumes, lengths, power, density, flow rates, pressure, etc.
|
||||
|
||||
To ensure the appropriate data type is used for properties, a property
|
||||
set template may be used. These can be seen as "property
|
||||
specifications". A default selection is provided by buildingSMART, so
|
||||
that all buildingSMART defined standard properties have exactly the same
|
||||
data types and exactly the right property names without fear of invalid
|
||||
data or typos. The built-in buildingSMART templates are always loaded.
|
||||
However, you may also specify your own templates. If you try to add a
|
||||
non-standard property that does not exist in either your own template or
|
||||
in the built-in buildingSMART template, then you have the responsibility
|
||||
to ensure that data types are always consistent and correct.
|
||||
|
||||
:param pset: The IfcPropertySet to edit.
|
||||
:param name: A new name for the property set. If no name is specified,
|
||||
the property set name is not changed.
|
||||
:param properties: A dictionary of properties. The keys must be a string
|
||||
of the name of the property. The data type of the value will be
|
||||
determined by the property set template. If no property set
|
||||
template is found, the data types of the Python values will
|
||||
influence the IFC data type of the property. String values will
|
||||
become IfcLabel, float values will become IfcReal, booleans will
|
||||
become IfcBoolean, and integers will become IfcInteger. If more
|
||||
control is desired, you may explicitly specify IFC data objects
|
||||
directly. Note that provided `properties` might be mutated in the process.
|
||||
:param pset_template: If a property set template is provided, this will
|
||||
be used to determine data types. If no user-defined template is
|
||||
provided, the built-in buildingSMART templates will be loaded.
|
||||
:param should_purge: If set as False, properties set to None will be
|
||||
left as None but not removed. If set to true, properties set to None
|
||||
will actually be removed. The default of true is the same behaviour as
|
||||
:func:`ifcopenshell.api.pset.edit_qto`.
|
||||
:return: None
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Let's imagine we have a new wall type.
|
||||
wall_type = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWallType")
|
||||
|
||||
# This is a standard buildingSMART property set.
|
||||
pset = ifcopenshell.api.pset.add_pset(model, product=wall_type, name="Pset_WallCommon")
|
||||
|
||||
# In this scenario, we don't specify any pset_template because it is
|
||||
# part of the built-in buildingSMART templates, and so the
|
||||
# FireRating will automatically be an IfcLabel, and the thermal
|
||||
# transmittance value will automatically be an
|
||||
# IfcThermalTransmittanceMeasure. Neither of these properties exist
|
||||
# yet, so they will be created.
|
||||
ifcopenshell.api.pset.edit_pset(model,
|
||||
pset=pset, properties={"FireRating": "2HR", "ThermalTransmittance": 42.3})
|
||||
|
||||
# We can edit existing properties. In this case, "FireRating" is
|
||||
# edited from "2HR" to "1HR". Combustible is new, and will be added.
|
||||
# The existing "ThermalTransmittance" property will be left
|
||||
# unchanged.
|
||||
ifcopenshell.api.pset.edit_pset(model,
|
||||
pset=pset, properties={"FireRating": "1HR", "Combustible": False})
|
||||
|
||||
# Setting to None will change the value but not delete the property.
|
||||
ifcopenshell.api.pset.edit_pset(model, pset=pset, properties={"Combustible": None})
|
||||
|
||||
# If you actually want to delete the property, enable purging.
|
||||
ifcopenshell.api.pset.edit_pset(model, pset=pset,
|
||||
properties={"Combustible": None}, should_purge=True)
|
||||
|
||||
# What if we wanted to manage our own properties? Let's create our
|
||||
# own "Company Standard" property set templates. Notice how we
|
||||
# prefix our property set with "Foo_", if our company name was "Foo"
|
||||
# this would make sense.
|
||||
template = ifcopenshell.api.pset_template.add_pset_template(model, name="Foo_bar")
|
||||
|
||||
# Let's imagine we want all model authors to specify two properties,
|
||||
# one being a length measurement and another being a boolean.
|
||||
prop1 = ifcopenshell.api.pset_template.add_prop_template(model,
|
||||
pset_template=template, name="DemoA", primary_measure_type="IfcLengthMeasure")
|
||||
prop2 = ifcopenshell.api.pset_template.add_prop_template(model,
|
||||
pset_template=template, name="DemoB", primary_measure_type="IfcBoolean")
|
||||
|
||||
# Now we can use our property set template to add our properties,
|
||||
# and the data types will always match our template.
|
||||
pset = ifcopenshell.api.pset.add_pset(model, product=wall_type, name="Foo_Bar")
|
||||
ifcopenshell.api.pset.edit_pset(model,
|
||||
pset=pset, properties={"DemoA": 42.3, "DemoB": True}, pset_template=template)
|
||||
|
||||
# Here's a third scenario where we want to add arbitrary properties
|
||||
# that are not standardised by anything, not even our own custom
|
||||
# templates.
|
||||
pset = ifcopenshell.api.pset.add_pset(model, product=wall_type, name="Custom_Pset")
|
||||
ifcopenshell.api.pset.edit_pset(model,
|
||||
pset=pset, properties={
|
||||
# Basic Python data types are mapped to a sensible default
|
||||
"SomeLabel": "Foo",
|
||||
"SomeNumber": 12.3,
|
||||
# But we can always specify exactly what we're after too
|
||||
"ExplicitLength": model.createIfcLengthMeasure(42.3)
|
||||
})
|
||||
|
||||
# Editing existing properties will retain their current data types
|
||||
# if possible. So this will still be a length measure.
|
||||
ifcopenshell.api.pset.edit_pset(model, pset=pset, properties={"ExplicitLength": 12.3})
|
||||
"""
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
usecase.settings = {
|
||||
"pset": pset,
|
||||
"name": name,
|
||||
"properties": properties or {},
|
||||
"pset_template": pset_template,
|
||||
"should_purge": should_purge,
|
||||
}
|
||||
return usecase.execute()
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
settings: dict[str, Any]
|
||||
|
||||
def execute(self) -> None:
|
||||
self.update_pset_name()
|
||||
self.load_pset_template()
|
||||
existing_props = self.update_existing_properties()
|
||||
new_props = self.add_new_properties()
|
||||
self.assign_new_properties(existing_props + new_props)
|
||||
|
||||
def update_pset_name(self) -> None:
|
||||
if self.settings["name"]:
|
||||
self.settings["pset"].Name = self.settings["name"]
|
||||
|
||||
def load_pset_template(self) -> None:
|
||||
if self.settings["pset_template"]:
|
||||
self.pset_template = self.settings["pset_template"]
|
||||
else:
|
||||
self.psetqto = ifcopenshell.util.pset.get_template(self.file.schema_identifier)
|
||||
self.pset_template = self.psetqto.get_by_name(self.settings["pset"].Name)
|
||||
|
||||
def _should_update_prop(self, prop: ifcopenshell.entity_instance) -> bool:
|
||||
"""
|
||||
Checks if the given property should be changed
|
||||
"""
|
||||
return prop.Name in self.settings["properties"]
|
||||
|
||||
def _try_purge(self, prop: ifcopenshell.entity_instance) -> bool:
|
||||
"""
|
||||
Tries to remove the property
|
||||
if successful, returns True, otherwise False
|
||||
NOTE: Assumes the prop exists
|
||||
"""
|
||||
if not self.settings["should_purge"]:
|
||||
return False
|
||||
|
||||
del self.settings["properties"][prop.Name]
|
||||
self.file.remove(prop)
|
||||
return True
|
||||
|
||||
# TODO - Add support for changing property types?
|
||||
# For example - IfcPropertyEnumeratedValue to
|
||||
# IfcPropertySingleValue. Or maybe the user should
|
||||
# just delete the property first? - vulevukusej
|
||||
def update_existing_properties(self) -> list[ifcopenshell.entity_instance]:
|
||||
existing_props = []
|
||||
for prop in self.get_properties():
|
||||
if not self._should_update_prop(prop):
|
||||
existing_props.append(prop)
|
||||
continue
|
||||
|
||||
if self.file.get_total_inverses(prop) > 1:
|
||||
continue # Treat as a new property to avoid affecting other psets.
|
||||
|
||||
if prop.is_a("IfcPropertyEnumeratedValue"):
|
||||
prop = self.update_existing_prop_enum(prop)
|
||||
if prop:
|
||||
existing_props.append(prop)
|
||||
elif prop.is_a("IfcPropertySingleValue"):
|
||||
prop = self.update_existing_prop_single_value(prop)
|
||||
if prop:
|
||||
existing_props.append(prop)
|
||||
else:
|
||||
raise NotImplementedError(f"Updating '{prop.is_a()}' properties is not supported yet")
|
||||
return existing_props
|
||||
|
||||
def update_existing_prop_enum(
|
||||
self, prop: ifcopenshell.entity_instance
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""
|
||||
NOTE: Assumes the prop exists
|
||||
"""
|
||||
value = self.settings["properties"][prop.Name]
|
||||
unit, value = self.unpack_unit_value(value)
|
||||
|
||||
if isinstance(value, (tuple, list)):
|
||||
sel_vals = []
|
||||
if not value:
|
||||
if self._try_purge(prop):
|
||||
return
|
||||
# Only need the first enum type since all enums are of the same type.
|
||||
if reference := prop.EnumerationReference:
|
||||
primary_measure_type = reference.EnumerationValues[0].is_a()
|
||||
elif enum_values := prop.EnumerationValues:
|
||||
primary_measure_type = enum_values[0].is_a()
|
||||
else:
|
||||
primary_measure_type = self.get_primary_measure_type(prop.Name, new_value=value[0])
|
||||
assert primary_measure_type, f"Couldn't find primary measure type for the prop value: '{value[0]}'."
|
||||
for val in value:
|
||||
ifc_val = self.file.create_entity(primary_measure_type, val)
|
||||
sel_vals.append(ifc_val)
|
||||
prop.EnumerationValues = tuple(sel_vals) or None
|
||||
|
||||
elif isinstance(value, ifcopenshell.entity_instance) and value.is_a("IfcPropertyEnumeratedValue"):
|
||||
# Copy enum value.
|
||||
if (value_enum_values := value.EnumerationValues) is None:
|
||||
if self._try_purge(prop):
|
||||
return
|
||||
prop.EnumerationValues = value_enum_values
|
||||
|
||||
# Copy values from / recreate value EnumerationReference in prop.
|
||||
value_reference = value.EnumerationReference
|
||||
prop_reference = prop.EnumerationReference
|
||||
if value_reference is None:
|
||||
if prop_reference:
|
||||
ifcopenshell.util.element.remove_deep2(self.file, prop_reference)
|
||||
prop.EnumerationReference = None
|
||||
else:
|
||||
if prop_reference is None:
|
||||
prop_reference = ifcopenshell.util.element.copy_deep(self.file, value_reference)
|
||||
prop.EnumerationReference = prop_reference
|
||||
else:
|
||||
prop_reference.Name = value_reference.Name
|
||||
prop_reference.EnumerationValues = value_reference.EnumerationValues
|
||||
prop_reference.Unit = value_reference.Unit
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
f'Value "{self.settings["properties"][prop.Name]}" is not a valid value for enum property {prop.Name}.'
|
||||
)
|
||||
|
||||
if unit:
|
||||
prop.Unit = unit
|
||||
del self.settings["properties"][prop.Name]
|
||||
return prop
|
||||
|
||||
def update_existing_prop_single_value(
|
||||
self, prop: ifcopenshell.entity_instance
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""
|
||||
NOTE: Assumes the prop exists
|
||||
"""
|
||||
value = self.settings["properties"][prop.Name]
|
||||
unit, value = self.unpack_unit_value(value)
|
||||
if value is None:
|
||||
if self._try_purge(prop):
|
||||
return
|
||||
prop.NominalValue = None
|
||||
elif isinstance(value, ifcopenshell.entity_instance):
|
||||
prop.NominalValue = value
|
||||
else:
|
||||
primary_measure_type = self.get_primary_measure_type(
|
||||
prop.Name, old_value=prop.NominalValue, new_value=value
|
||||
)
|
||||
value = self.cast_value_to_primary_measure_type(value, primary_measure_type)
|
||||
prop.NominalValue = self.file.create_entity(primary_measure_type, value)
|
||||
if unit:
|
||||
prop.Unit = unit
|
||||
del self.settings["properties"][prop.Name]
|
||||
return prop
|
||||
|
||||
def add_new_properties(self) -> list[ifcopenshell.entity_instance]:
|
||||
properties: list[ifcopenshell.entity_instance] = []
|
||||
for name, value in self.settings["properties"].items():
|
||||
if value is None and self.settings["should_purge"]:
|
||||
continue
|
||||
unit, value = self.unpack_unit_value(value)
|
||||
|
||||
if isinstance(value, ifcopenshell.entity_instance):
|
||||
if value.is_a("IfcProperty"):
|
||||
properties.append(value)
|
||||
|
||||
# If it's not an entity, then it's a primitive data type
|
||||
elif not value.is_entity():
|
||||
kwargs = {"Name": name, "NominalValue": value}
|
||||
if unit:
|
||||
kwargs["Unit"] = unit
|
||||
properties.append(self.file.create_entity("IfcPropertySingleValue", **kwargs))
|
||||
|
||||
else:
|
||||
raise ValueError(f"{value.is_a()} cannot be assigned to the property set '{name}'")
|
||||
|
||||
elif isinstance(value, (tuple, list)):
|
||||
if not value:
|
||||
continue
|
||||
for pset_template in self.pset_template.HasPropertyTemplates:
|
||||
if pset_template.Name != name:
|
||||
continue
|
||||
|
||||
if pset_template.TemplateType == "P_LISTVALUE":
|
||||
ifc_class = getattr(pset_template, "PrimaryMeasureType", None)
|
||||
if ifc_class is None:
|
||||
raise ValueError(f"pset template '{pset_template.Name}' is missing PrimaryMeasureType")
|
||||
|
||||
properties.append(
|
||||
self.file.create_entity(
|
||||
"IfcPropertyListValue",
|
||||
Name=name,
|
||||
ListValues=[self.file.create_entity(ifc_class, v) for v in value],
|
||||
Unit=unit,
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
elif pset_template.TemplateType == "P_ENUMERATEDVALUE":
|
||||
prop_enum = self.file.create_entity(
|
||||
"IFCPROPERTYENUMERATION",
|
||||
Name=name,
|
||||
EnumerationValues=pset_template.Enumerators.EnumerationValues,
|
||||
**({"Unit": unit} if unit else {}),
|
||||
)
|
||||
prop_enum_value = self.file.create_entity(
|
||||
"IFCPROPERTYENUMERATEDVALUE",
|
||||
Name=name,
|
||||
EnumerationValues=tuple(
|
||||
self.file.create_entity(pset_template.PrimaryMeasureType, v) for v in value
|
||||
),
|
||||
EnumerationReference=prop_enum,
|
||||
)
|
||||
properties.append(prop_enum_value)
|
||||
break
|
||||
|
||||
raise NotImplementedError(f"Template type '{pset_template.TemplateType}' is not supported yet")
|
||||
|
||||
else:
|
||||
raise NotImplementedError(f"No template found for property '{name}'")
|
||||
|
||||
else:
|
||||
primary_measure_type = self.get_primary_measure_type(name, new_value=value)
|
||||
if value is None:
|
||||
nominal_value = value
|
||||
else:
|
||||
value = self.cast_value_to_primary_measure_type(value, primary_measure_type)
|
||||
nominal_value = self.file.create_entity(primary_measure_type, value)
|
||||
args = {"Name": name, "NominalValue": nominal_value}
|
||||
if unit:
|
||||
args["Unit"] = unit
|
||||
|
||||
properties.append(self.file.create_entity("IfcPropertySingleValue", **args))
|
||||
return properties
|
||||
|
||||
def assign_new_properties(self, props: list[ifcopenshell.entity_instance]) -> None:
|
||||
if hasattr(self.settings["pset"], "HasProperties"):
|
||||
self.settings["pset"].HasProperties = props
|
||||
|
||||
# Material / Profile properties
|
||||
elif hasattr(self.settings["pset"], "Properties"):
|
||||
self.settings["pset"].Properties = props
|
||||
|
||||
# IFC2X3 IfcMaterialProperties
|
||||
elif self.settings["pset"].is_a("IfcMaterialProperties"):
|
||||
self.settings["pset"].ExtendedProperties = props
|
||||
|
||||
def get_properties(self) -> list[ifcopenshell.entity_instance]:
|
||||
"""
|
||||
Returns list of existing properties
|
||||
"""
|
||||
if (props := getattr(self.settings["pset"], "HasProperties", ...)) is not ...:
|
||||
return props or []
|
||||
|
||||
# Material / Profile properties
|
||||
elif (props := getattr(self.settings["pset"], "Properties", ...)) is not ...:
|
||||
return props or []
|
||||
|
||||
# IFC2X3 IfcMaterialProperties
|
||||
elif (props := getattr(self.settings["pset"], "ExtendedProperties", ...)) is not ...:
|
||||
return props or []
|
||||
|
||||
raise TypeError(f"'{self.settings['pset']}' is not a valid pset")
|
||||
|
||||
def get_primary_measure_type(
|
||||
self,
|
||||
name: str,
|
||||
old_value: Optional[ifcopenshell.entity_instance] = None,
|
||||
new_value: Optional[Union[ifcopenshell.entity_instance, str, float, bool, int]] = None,
|
||||
) -> Union[str, None]:
|
||||
if old_value:
|
||||
return old_value.is_a()
|
||||
if self.pset_template:
|
||||
for prop_template in self.pset_template.HasPropertyTemplates:
|
||||
if prop_template.Name != name:
|
||||
continue
|
||||
return prop_template.PrimaryMeasureType or "IfcLabel"
|
||||
if isinstance(new_value, ifcopenshell.entity_instance):
|
||||
return new_value.is_a()
|
||||
elif new_value is not None:
|
||||
if isinstance(new_value, str):
|
||||
return "IfcLabel"
|
||||
elif isinstance(new_value, float):
|
||||
return "IfcReal"
|
||||
elif isinstance(new_value, bool):
|
||||
return "IfcBoolean"
|
||||
elif isinstance(new_value, int):
|
||||
return "IfcInteger"
|
||||
# @nb datetime is also a date, so needs to be checked first
|
||||
elif isinstance(new_value, datetime.datetime):
|
||||
return "IfcDateTime"
|
||||
elif isinstance(new_value, datetime.date):
|
||||
return "IfcDate"
|
||||
|
||||
def cast_value_to_primary_measure_type(self, value, primary_measure_type):
|
||||
type_str = self.file.create_entity(primary_measure_type).attribute_type(0)
|
||||
type_fn = {
|
||||
"AGGREGATE OF DOUBLE": list,
|
||||
"AGGREGATE OF INT": list,
|
||||
"AGGREGATE OF ENTITY INSTANCE": list,
|
||||
"BINARY": bytes,
|
||||
"LOGICAL": str,
|
||||
"BOOL": bool,
|
||||
"INT": int,
|
||||
"DOUBLE": float,
|
||||
"STRING": str,
|
||||
}[type_str]
|
||||
if type_str == "AGGREGATE OF DOUBLE":
|
||||
return [float(i) for i in value]
|
||||
elif type_str == "AGGREGATE OF INT":
|
||||
return [int(i) for i in value]
|
||||
elif isinstance(value, (datetime.date, datetime.datetime)):
|
||||
return value.isoformat()
|
||||
return type_fn(value)
|
||||
|
||||
@staticmethod
|
||||
def unpack_unit_value(value_candidate):
|
||||
"""
|
||||
Returns tuple of the format: (Unit, NominalValue)
|
||||
NOTE: Unit fallbacks to None
|
||||
"""
|
||||
if value_candidate is None:
|
||||
return (None, None)
|
||||
|
||||
if isinstance(value_candidate, dict): # Custom IfcUnits can be passed in a dict along with the pset value
|
||||
return (value_candidate["Unit"], value_candidate["NominalValue"])
|
||||
|
||||
return (None, value_candidate)
|
||||
@@ -0,0 +1,244 @@
|
||||
# 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
|
||||
|
||||
from typing_extensions import assert_never
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.pset
|
||||
|
||||
FLOAT_TYPE_KEYWORDS = (
|
||||
("Area", ("area",)),
|
||||
("Volume", ("volume",)),
|
||||
("Weight", ("weight", "mass")),
|
||||
("Length", ("length", "width", "height", "depth", "distance")),
|
||||
("Time", ("time", "duration")),
|
||||
)
|
||||
|
||||
PROP_VALUE_TYPE = Union[ifcopenshell.entity_instance, float, int, dict[str, "PROP_VALUE_TYPE"]]
|
||||
|
||||
|
||||
def edit_qto(
|
||||
file: ifcopenshell.file,
|
||||
qto: ifcopenshell.entity_instance,
|
||||
name: Optional[str] = None,
|
||||
properties: Optional[dict[str, PROP_VALUE_TYPE]] = None,
|
||||
pset_template: Optional[ifcopenshell.entity_instance] = None,
|
||||
) -> None:
|
||||
"""Edits a quantity set and its quantities
|
||||
|
||||
At its simplest usage, this may be used to edit the name of a quantity
|
||||
set. It may also be used to add, edit, or remove quantities.
|
||||
|
||||
See ifcopenshell.api.pset.edit_pset for documentation on how this is
|
||||
intended to be used.
|
||||
|
||||
One major difference is that quantities set to None are always purged.
|
||||
It is not allowed to have None quantities in IFC.
|
||||
|
||||
:param qto: The IfcElementQuantity or IfcPhysicalComplexQuantity to edit.
|
||||
:param name: A new name for the quantity set. If no name is specified,
|
||||
the quantity set name is not changed.
|
||||
:param properties: A dictionary of properties. The keys must be a string
|
||||
of the name of the quantity. The data type of the value will be
|
||||
determined by the quantity set template. If no quantity set
|
||||
template is found, the data types of the Python values and
|
||||
properties names will influence the IFC data type of the quantity.
|
||||
|
||||
- For `float` values - see `FLOAT_TYPE_KEYWORDS` for the keywords in property name
|
||||
used to detect the quantity type. If no keyword matches,
|
||||
the default quantity type will be IfcQuantityLength.
|
||||
|
||||
- `int` values will map to IfcQuantityCount.
|
||||
|
||||
- dictionary values will be used to create IfcPhysicalComplexQuantity with
|
||||
properties from the dictionary.
|
||||
|
||||
If more control is desired, you may explicitly specify IFC data objects directly.
|
||||
:param pset_template: If a quantity set template is provided, this will
|
||||
be used to determine data types. If no user-defined template is
|
||||
provided, the built-in buildingSMART templates will be loaded.
|
||||
:return: None
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Let's imagine we have a new wall type.
|
||||
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
|
||||
|
||||
# This is a standard buildingSMART property set.
|
||||
qto = ifcopenshell.api.pset.add_qto(model, product=wall, name="Qto_WallBaseQuantities")
|
||||
|
||||
# In this scenario, we don't specify any pset_template because it is
|
||||
# part of the built-in buildingSMART templates, and so the Length
|
||||
# will automatically be an IfcLengthMeasure, and the NetVolume will
|
||||
# automatically be an IfcVolumeMeasure. Neither of these properties
|
||||
# exist yet, so they will be created.
|
||||
ifcopenshell.api.pset.edit_qto(model, qto=qto, properties={"Length": 12, "NetVolume": 7.2})
|
||||
|
||||
# Setting to None will delete the quantity.
|
||||
ifcopenshell.api.pset.edit_qto(model, qto=qto, properties={"Length": None})
|
||||
|
||||
# What if we wanted to manage our own properties? Let's create our
|
||||
# own "Company Standard" property set templates. Notice how we
|
||||
# prefix our property set with "Foo_", if our company name was "Foo"
|
||||
# this would make sense. In this example, we say that our template
|
||||
# only applies to walls and is for quantities.
|
||||
template = ifcopenshell.api.pset_template.add_pset_template(model,
|
||||
name="Foo_Wall", template_type="QTO_OCCURRENCEDRIVEN", applicable_entity="IfcWall")
|
||||
|
||||
# Let's imagine we want all model authors to specify a length
|
||||
# measurement for the portion of a wall that is overhanging.
|
||||
prop = ifcopenshell.api.pset_template.add_prop_template(model, pset_template=template,
|
||||
name="OverhangLength", template_type="Q_LENGTH", primary_measure_type="IfcLengthMeasure")
|
||||
|
||||
# Now we can use our property set template to add our properties,
|
||||
# and the data types will always match our template.
|
||||
qto = ifcopenshell.api.pset.add_qto(model, product=wall, name="Foo_Wall")
|
||||
ifcopenshell.api.pset.edit_qto(model,
|
||||
qto=qto, properties={"OverhangLength": 42.3}, pset_template=template)
|
||||
|
||||
# Here's a third scenario where we want to add arbitrary quantities
|
||||
# that are not standardised by anything, not even our own custom
|
||||
# templates.
|
||||
qto = ifcopenshell.api.pset.add_qto(model, product=wall, name="Custom_Qto")
|
||||
ifcopenshell.api.pset.edit_qto(model,
|
||||
qto=qto, properties={
|
||||
"SomeLength": model.createIfcLengthMeasure(42.3),
|
||||
"SomeArea": model.createIfcAreaMeasure(21.0)
|
||||
})
|
||||
|
||||
# Editing existing quantities will retain their current data types
|
||||
# if possible. So this will still be a length measure.
|
||||
ifcopenshell.api.pset.edit_qto(model, qto=qto, properties={"SomeLength": 12.3})
|
||||
"""
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
usecase.settings = {"qto": qto, "name": name, "properties": properties or {}, "pset_template": pset_template}
|
||||
return usecase.execute()
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
settings: dict[str, Any]
|
||||
|
||||
def execute(self):
|
||||
self.qto_idx = 5
|
||||
if self.settings["qto"].is_a("IfcPhysicalComplexQuantity"):
|
||||
self.qto_idx = 2
|
||||
|
||||
self.update_qto_name()
|
||||
self.load_qto_template()
|
||||
self.update_existing_properties()
|
||||
new_properties = self.add_new_properties()
|
||||
self.extend_qto_with_new_properties(new_properties)
|
||||
|
||||
def update_qto_name(self) -> None:
|
||||
if self.settings["name"]:
|
||||
self.settings["qto"].Name = self.settings["name"]
|
||||
|
||||
def load_qto_template(self) -> None:
|
||||
if self.settings["pset_template"]:
|
||||
self.pset_template = self.settings["pset_template"]
|
||||
else:
|
||||
self.psetqto = ifcopenshell.util.pset.get_template(self.file.schema_identifier)
|
||||
self.qto_template = self.psetqto.get_by_name(self.settings["qto"].Name)
|
||||
|
||||
def update_existing_properties(self) -> None:
|
||||
for prop in self.settings["qto"][self.qto_idx] or []:
|
||||
self.update_existing_property(prop)
|
||||
|
||||
def update_existing_property(self, prop: ifcopenshell.entity_instance) -> None:
|
||||
if prop.Name not in self.settings["properties"]:
|
||||
return
|
||||
value = self.settings["properties"][prop.Name]
|
||||
name = prop.Name
|
||||
if value is None:
|
||||
self.file.remove(prop)
|
||||
elif prop.is_a("IfcPhysicalComplexQuantity") and isinstance(value, dict):
|
||||
prop.Discrimination = value.get("Discrimination", prop.Discrimination)
|
||||
ifcopenshell.api.pset.edit_qto(self.file, qto=prop, properties=value["HasQuantities"])
|
||||
elif prop.is_a("IfcPhysicalSimpleQuantity"):
|
||||
value = value.wrappedValue if isinstance(value, ifcopenshell.entity_instance) else value
|
||||
# 3 IfcPhysicalSimpleQuantity.XXXValue
|
||||
if self.file.schema == "IFC4X3" and prop.is_a("IfcQuantityCount"):
|
||||
prop[3] = int(value)
|
||||
else:
|
||||
prop[3] = float(value)
|
||||
del self.settings["properties"][name]
|
||||
|
||||
def add_new_properties(self) -> list[ifcopenshell.entity_instance]:
|
||||
properties = []
|
||||
for name, value in self.settings["properties"].items():
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, dict):
|
||||
complex_qto = self.file.create_entity(
|
||||
"IfcPhysicalComplexQuantity", Name=name, Discrimination=value["Discrimination"]
|
||||
)
|
||||
properties.append(complex_qto)
|
||||
ifcopenshell.api.pset.edit_qto(self.file, qto=complex_qto, properties=value["HasQuantities"])
|
||||
else:
|
||||
property_type = self.get_canonical_property_type(name, value)
|
||||
value = value.wrappedValue if isinstance(value, ifcopenshell.entity_instance) else value
|
||||
properties.append(
|
||||
self.file.create_entity(
|
||||
"IfcQuantity{}".format(property_type),
|
||||
**{"Name": name, "{}Value".format(property_type): value},
|
||||
)
|
||||
)
|
||||
return properties
|
||||
|
||||
def extend_qto_with_new_properties(self, new_properties: list[ifcopenshell.entity_instance]) -> None:
|
||||
props = list(self.settings["qto"][self.qto_idx]) if self.settings["qto"][self.qto_idx] else []
|
||||
props.extend(new_properties)
|
||||
self.settings["qto"][self.qto_idx] = props
|
||||
|
||||
def get_canonical_property_type(self, name: str, value: Union[ifcopenshell.entity_instance, float, int]) -> str:
|
||||
if isinstance(value, ifcopenshell.entity_instance):
|
||||
result = value.is_a().replace("Ifc", "").replace("Measure", "")
|
||||
# Sigh, IFC inconsistencies
|
||||
if result == "Numeric":
|
||||
result = "Number"
|
||||
elif result == "Mass":
|
||||
result = "Weight"
|
||||
return result
|
||||
if self.qto_template:
|
||||
for prop_template in self.qto_template.HasPropertyTemplates:
|
||||
if prop_template.Name != name:
|
||||
continue
|
||||
return prop_template.TemplateType[2:].lower().capitalize()
|
||||
return infer_property_type(name, value)
|
||||
|
||||
|
||||
def infer_property_type(name: str, value: Union[float, int]) -> str:
|
||||
name_lower = name.lower()
|
||||
# Only undetected type is IfcQuantityNumber (IFC4X3),
|
||||
# not sure when it's appropriate.
|
||||
if isinstance(value, float):
|
||||
for category, keywords in FLOAT_TYPE_KEYWORDS:
|
||||
if any(keyword in name_lower for keyword in keywords):
|
||||
return category
|
||||
return "Length"
|
||||
elif isinstance(value, int):
|
||||
return "Count"
|
||||
else:
|
||||
assert_never(value)
|
||||
@@ -0,0 +1,81 @@
|
||||
# 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_pset(
|
||||
file: ifcopenshell.file, product: ifcopenshell.entity_instance, pset: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
"""Removes a property set from a product
|
||||
|
||||
All properties that are part of this property set are also removed.
|
||||
|
||||
:param product: The IfcObject to remove the property set from.
|
||||
:param pset: The IfcPropertySet or IfcElementQuantity to remove.
|
||||
:return: None
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Let's imagine we have a new wall type with a property set.
|
||||
wall_type = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWallType")
|
||||
pset = ifcopenshell.api.pset.add_pset(model, product=wall_type, name="Pset_WallCommon")
|
||||
|
||||
# Remove it!
|
||||
ifcopenshell.api.pset.remove_pset(model, product=wall_type, pset=pset)
|
||||
"""
|
||||
to_purge = []
|
||||
should_remove_pset = True
|
||||
for inverse in file.get_inverse(pset):
|
||||
if inverse.is_a("IfcRelDefinesByProperties"):
|
||||
if not inverse.RelatedObjects or len(inverse.RelatedObjects) == 1:
|
||||
to_purge.append(inverse)
|
||||
else:
|
||||
related_objects = list(inverse.RelatedObjects)
|
||||
related_objects.remove(product)
|
||||
inverse.RelatedObjects = related_objects
|
||||
should_remove_pset = False
|
||||
if should_remove_pset:
|
||||
properties = [] # Predefined psets have no properties
|
||||
if pset.is_a("IfcPropertySet"):
|
||||
properties = pset.HasProperties or []
|
||||
elif pset.is_a("IfcQuantitySet"):
|
||||
properties = pset.Quantities or []
|
||||
elif pset.is_a() in ("IfcMaterialProperties", "IfcProfileProperties"):
|
||||
properties = pset.Properties or []
|
||||
for prop in properties:
|
||||
if file.get_total_inverses(prop) != 1:
|
||||
continue
|
||||
if prop.is_a("IfcPropertyEnumeratedValue"):
|
||||
enumeration = prop.EnumerationReference
|
||||
if enumeration and file.get_total_inverses(enumeration) == 1:
|
||||
file.remove(enumeration)
|
||||
file.remove(prop)
|
||||
# IfcMaterialProperties and IfcProfileProperties don't have OwnerHistory
|
||||
history = getattr(pset, "OwnerHistory", None)
|
||||
file.remove(pset)
|
||||
if history:
|
||||
ifcopenshell.util.element.remove_deep2(file, history)
|
||||
for element in to_purge:
|
||||
history = getattr(element, "OwnerHistory", None)
|
||||
file.remove(element)
|
||||
if history:
|
||||
ifcopenshell.util.element.remove_deep2(file, history)
|
||||
@@ -0,0 +1,78 @@
|
||||
# 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 unassign_pset(
|
||||
file: ifcopenshell.file,
|
||||
products: list[ifcopenshell.entity_instance],
|
||||
pset: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
"""Unassign property set from the provided elements.
|
||||
|
||||
:param products: Elements (or element types) to assign the pset from.
|
||||
:param pset: Property set.
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
element1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||
element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||
ifcopenshell.api.pset.assign_pset(self.file, [element1, element2], pset)
|
||||
|
||||
# Pset is now shared by 2 elements.
|
||||
assert ifcopenshell.util.element.get_elements_by_pset(pset) == {element1, element2}
|
||||
|
||||
ifcopenshell.api.pset.unassign_pset(self.file, [element2], pset)
|
||||
# Pset was unassigned from element2.
|
||||
assert ifcopenshell.util.element.get_elements_by_pset(pset) == {element1}
|
||||
|
||||
"""
|
||||
is_ifc2x3 = file.schema == "IFC2X3"
|
||||
|
||||
products_occurrences: set[ifcopenshell.entity_instance] = set()
|
||||
products_types: set[ifcopenshell.entity_instance] = set()
|
||||
for product in products:
|
||||
if product.is_a("IfcTypeProduct"):
|
||||
products_types.add(product)
|
||||
else:
|
||||
products_occurrences.add(product)
|
||||
|
||||
# Check occurrences using pset.
|
||||
if products_occurrences:
|
||||
rels = pset.PropertyDefinitionOf if is_ifc2x3 else pset.DefinesOccurrence
|
||||
for rel in rels:
|
||||
objs = set(rel.RelatedObjects)
|
||||
if not any(p in objs for p in products_occurrences):
|
||||
continue
|
||||
objs.difference_update(products_occurrences)
|
||||
if objs:
|
||||
rel.RelatedObjects = list(objs)
|
||||
else:
|
||||
history = rel.OwnerHistory
|
||||
file.remove(rel)
|
||||
if history:
|
||||
ifcopenshell.util.element.remove_deep2(file, history)
|
||||
|
||||
for product in products_types:
|
||||
psets = list(product.HasPropertySets)
|
||||
psets.remove(pset)
|
||||
product.HasPropertySets = psets or None
|
||||
@@ -0,0 +1,93 @@
|
||||
# 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.pset
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def unshare_pset(
|
||||
file: ifcopenshell.file,
|
||||
products: list[ifcopenshell.entity_instance],
|
||||
pset: ifcopenshell.entity_instance,
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
"""Copy a shared pset as linked only to the provided elements.
|
||||
|
||||
Note that method will create a copy of the pset for each element provided.
|
||||
|
||||
:param products: Elements (or element types) to link the pset to.
|
||||
:param pset: Shared property set.
|
||||
:return: List of copied property sets.
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
element1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||
element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||
ifcopenshell.api.pset.assign_pset(self.file, [element1, element2], pset)
|
||||
|
||||
# Pset is now shared by 2 elements.
|
||||
assert ifcopenshell.util.element.get_elements_by_pset(pset) == {element1, element2}
|
||||
|
||||
new_psets = ifcopenshell.api.pset.unshare_pset(self.file, [element2], pset)
|
||||
|
||||
# element2 was unassigned from the original pset.
|
||||
assert ifcopenshell.util.element.get_elements_by_pset(pset) == {element1}
|
||||
new_pset = new_psets[0]
|
||||
|
||||
# New pset was created and was assigned to element2.
|
||||
assert new_pset != pset
|
||||
assert ifcopenshell.util.element.get_elements_by_pset(new_pset) == {element2}
|
||||
"""
|
||||
|
||||
if not products:
|
||||
raise Exception("No products provided.")
|
||||
|
||||
# If pset has no other elements besides the provided products,
|
||||
# then we skip the first product, so it won't get additional pset copy
|
||||
# leaving the original pset orphaned.
|
||||
pset_elements = ifcopenshell.util.element.get_elements_by_pset(pset)
|
||||
products_original = products
|
||||
|
||||
if set(products) == pset_elements:
|
||||
products = products[1:]
|
||||
|
||||
if not products:
|
||||
raise Exception(f"Provided product is the only element to which pset is assigned: {products_original[0]}.")
|
||||
|
||||
products_occurrences: set[ifcopenshell.entity_instance] = set()
|
||||
products_types: set[ifcopenshell.entity_instance] = set()
|
||||
for product in products:
|
||||
if product.is_a("IfcTypeProduct"):
|
||||
products_types.add(product)
|
||||
else:
|
||||
products_occurrences.add(product)
|
||||
|
||||
ifcopenshell.api.pset.unassign_pset(file, products, pset)
|
||||
|
||||
pset_copies: list[ifcopenshell.entity_instance] = []
|
||||
for product in products:
|
||||
# No need to consider about profile/material properties since
|
||||
# they are assigned to 1 element directly and therefore cannot be shared.
|
||||
# Don't copy_deep to keep it light - edit_pset supports unsharing shared props.
|
||||
pset_copy = ifcopenshell.util.element.copy(file, pset)
|
||||
pset_copies.append(pset_copy)
|
||||
ifcopenshell.api.pset.assign_pset(file, [product], pset_copy)
|
||||
|
||||
return pset_copies
|
||||
Reference in New Issue
Block a user