First Commit
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user