Merge branch 'v0.7.0' into v0.8.0

This commit is contained in:
Dion Moult
2024-06-07 23:48:39 +10:00
776 changed files with 34282 additions and 27706 deletions
@@ -16,32 +16,51 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""The entry module for IfcOpenShell
"""Welcome to IfcOpenShell! IfcOpenShell provides a way to read and write IFCs.
Typically used for opening an IFC via a filepath, or accessing one of the
submodules.
IfcOpenShell can open IFC files, read entities (such as walls, buildings,
properties, systems, etc), edit attributes, write out ``.ifc`` files and more.
This module provides primitive functions to interact with IFC, including:
- For most users, you can open and read IFC models, see docs for :func:`open`.
This returns an :class:`file` object representing the IFC model. You can then
query the model to filter elements.
- For developers, you can query the schema itself, see docs for
:func:`schema_by_name`. This returns a schema object which you can use to
analyse the definitions of IFC classes and data types.
You may also be interested in:
- For model authoring and editing operations, see :mod:`ifcopenshell.api`.
- For extracting information from models, see :mod:`ifcopenshell.util`.
- For processing geometry, see :mod:`ifcopenshell.geom`.
For more details, consult https://docs.ifcopenshell.org/
Example:
.. code:: python
import ifcopenshell
print(ifcopenshell.version) # v0.7.0-1b1fd1e6
model = ifcopenshell.open("/path/to/model.ifc")
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
print(ifcopenshell.version) # v0.7.0-1b1fd1e6
model = ifcopenshell.open("/path/to/model.ifc")
walls = model.by_type("IfcWall")
for wall in walls:
print(wall.Name)
"""
import os
import sys
import tempfile
import zipfile
import tempfile
from pathlib import Path
from typing import Optional
from typing import Optional, Union
import ifcopenshell.util.file
if hasattr(os, "uname"):
platform_system = os.uname()[0].lower()
@@ -60,22 +79,29 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "lib", p
try:
from . import ifcopenshell_wrapper
except Exception as e:
if int(python_version_tuple[0]) == 2:
# Only for py2, as py3 has exception chaining
import traceback
traceback.print_exc()
print("-" * 64)
except Exception:
raise ImportError("IfcOpenShell not built for '%s'" % python_distribution)
from . import guid
from .file import file
from .entity_instance import entity_instance, register_schema_attributes
from .sql import sqlite, sqlite_entity
# explicitly specify available imported symbols
# (it's a requirement for a typed library)
__all__ = [
"ifcopenshell_wrapper",
"file",
"entity_instance",
"sqlite",
"sqlite_entity",
"stream",
"stream_entity",
]
try:
from .stream import stream, stream_entity
except: pass
except:
pass
READ_ERROR = ifcopenshell_wrapper.file_open_status.READ_ERROR
NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER
@@ -84,19 +110,22 @@ UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA
class Error(Exception):
"""Error used when a generic problem occurs"""
pass
class SchemaError(Error):
"""Error used when an IFC schema related problem occurs"""
pass
def open(path: "os.PathLike | str", format: str = None, should_stream: bool = False) -> file:
def open(path: Union[os.PathLike, str], format: Optional[str] = None, should_stream: bool = False) -> file:
"""Loads an IFC dataset from a filepath
You can specify a file format. If no format is given, it is guessed from its extension.
Currently supported specified format : .ifc | .ifcZIP | .ifcXML
You can specify a file format. If no format is given, it is guessed from
its extension. Currently supported specified format: .ifc | .ifcZIP |
.ifcXML.
You can then filter by element ID, class, etc, and subscript by id or guid.
@@ -114,7 +143,7 @@ def open(path: "os.PathLike | str", format: str = None, should_stream: bool = Fa
"""
path = Path(path)
if format is None:
format = ifcopenshell.util.file.guess_format(path)
format = guess_format(path)
if format == ".ifcXML":
f = ifcopenshell_wrapper.parse_ifcxml(str(path.absolute()))
if f:
@@ -141,8 +170,7 @@ def open(path: "os.PathLike | str", format: str = None, should_stream: bool = Fa
NO_HEADER: (Error, "Unable to parse IFC SPF header"),
UNSUPPORTED_SCHEMA: (
SchemaError,
"Unsupported schema: %s"
% ",".join(f.header.file_schema.schema_identifiers),
"Unsupported schema: %s" % ",".join(f.header.file_schema.schema_identifiers),
),
}[f.good().value()]
raise exc(msg)
@@ -152,7 +180,7 @@ def create_entity(type, schema="IFC4", *args, **kwargs):
"""Creates a new IFC entity that does not belong to an IFC file object
Note that it is more common to create entities within a existing file
object. See :meth:`ifcopenshell.file.file.create_entity`.
object. See :meth:`ifcopenshell.file.create_entity`.
:param type: Case insensitive name of the IFC class
:type type: string
@@ -161,7 +189,7 @@ def create_entity(type, schema="IFC4", *args, **kwargs):
:param args: The positional arguments of the IFC class
:param kwargs: The keyword arguments of the IFC class
:returns: An entity instance
:rtype: ifcopenshell.entity_instance.entity_instance
:rtype: ifcopenshell.entity_instance
Example:
@@ -226,4 +254,35 @@ def schema_by_name(
return ifcopenshell_wrapper.schema_by_name(schema)
from .main import *
def guess_format(path: Path) -> Union[str, None]:
"""Guesses the IFC format using file extension
IFCs may be serialised as different formats. The most common is a ``.ifc``
file, which is plaintext and stores data using the STEP Physical File
format. IFC can also be stored as a Zipfile, XML, JSON, or SQL.
This will return the canonical form of the format. For example, if a path
has the extension of .xml or .ifcxml (case insensitive), it will return
.ifcXML.
Users generally won't call this function. The :func:`open` function uses
this internally to guess the file format.
:return: Either .ifc, .ifcZIP, .ifcXML, .ifcJSON, .ifcSQLite, or None.
"""
suffix = path.suffix.lower()
if suffix == ".ifc":
return ".ifc"
elif suffix in (".ifczip", ".zip"):
return ".ifcZIP"
elif suffix in (".ifcxml", ".xml"):
return ".ifcXML"
elif suffix in (".ifcjson", ".json"):
return ".ifcJSON"
elif suffix in (".ifcsqlite", ".sqlite", ".db"):
return ".ifcSQLite"
return None
version = ifcopenshell_wrapper.version()
get_log = ifcopenshell_wrapper.get_log
@@ -16,19 +16,39 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""High level user-oriented IFC authoring capabilities"""
"""High level IFC authoring and editing functions
Authoring, editing, and deleting IFC data requires a detailed understanding of
the rules of the IFC schema. This API module provides simple to use authoring
functions that hide this complexity from you. Things like managing differences
between IFC versions, tracking owernship changes, or cleaning up after orphaned
relationships are all handled automatically.
If you're new to IFC authoring, start by looking at the following APIs:
- See :func:`ifcopenshell.api.project.create_file` to create a new IFC.
- See :func:`ifcopenshell.api.root.create_entity` to create new entities, like
the mandatory IfcProject, and then an IfcSite, IfcWall, etc.
- See :func:`ifcopenshell.api.aggregate.assign_object` to create a spatial
hierarchy.
- See :func:`ifcopenshell.api.spatial.assign_container` to place physical
elements (e.g. walls) inside spatial elements (e.g. building storeys).
Also see how to `create a simple model from scratch
<https://docs.ifcopenshell.org/ifcopenshell-python/code_examples.html#create-a-simple-model-from-scratch>`_.
"""
import json
import numpy
import inspect
import importlib
import ifcopenshell
import ifcopenshell.api
from typing import Callable, Any, Optional
from functools import partial
pre_listeners = {}
post_listeners = {}
pre_listeners: dict[str, dict] = {}
post_listeners: dict[str, dict] = {}
def batching_argument_deprecation(
@@ -47,6 +67,20 @@ def batching_argument_deprecation(
return (replace_usecase or usecase_path, settings)
def renamed_arguments_deprecation(
usecase_path: str, settings: dict, arguments_remapped: dict[str, str]
) -> tuple[str, dict]:
for prev_argument, new_argument in arguments_remapped.items():
if prev_argument in settings:
print(
f"WARNING. `{prev_argument}` argument is deprecated for API method "
f'"{usecase_path}" and should be replaced with `{new_argument}`.'
)
settings = settings | {new_argument: settings[prev_argument]}
settings.pop(prev_argument)
return (usecase_path, settings)
ARGUMENTS_DEPRECATION = {
"spatial.assign_container": partial(
batching_argument_deprecation, prev_argument="product", new_argument="products"
@@ -117,50 +151,43 @@ ARGUMENTS_DEPRECATION = {
"constraint.unassign_constraint": partial(
batching_argument_deprecation, prev_argument="product", new_argument="products"
),
"project.assign_declaration": partial(
batching_argument_deprecation, prev_argument="definition", new_argument="definitions"
),
"project.unassign_declaration": partial(
batching_argument_deprecation, prev_argument="definition", new_argument="definitions"
),
"group.add_group": partial(
renamed_arguments_deprecation, arguments_remapped={"Name": "name", "Description": "description"}
),
"layer.add_layer": partial(renamed_arguments_deprecation, arguments_remapped={"Name": "name"}),
}
CACHED_USECASE_CLASSES = dict()
CACHED_USECASE_CLASSES: dict[str, Callable] = {}
CACHED_USECASES: dict[str, Callable] = {}
def run(
usecase_path: str,
ifc_file: Optional[ifcopenshell.file] = None,
should_run_listeners=True,
should_run_listeners: bool = True,
**settings: Any,
) -> Any:
usecase_function = CACHED_USECASES.get(usecase_path)
if not usecase_function:
importlib.import_module(f"ifcopenshell.api.{usecase_path}")
module, usecase = usecase_path.split(".")
usecase_function = getattr(getattr(ifcopenshell.api, module), usecase)
CACHED_USECASES[usecase_path] = usecase_function
if ifc_file:
return usecase_function(ifc_file, should_run_listeners=should_run_listeners, **settings)
return usecase_function(should_run_listeners=should_run_listeners, **settings)
if should_run_listeners:
for listener in pre_listeners.get(usecase_path, {}).values():
listener(usecase_path, ifc_file, settings)
# see #4531
if usecase_path in ARGUMENTS_DEPRECATION:
usecase_path, settings = ARGUMENTS_DEPRECATION[usecase_path](usecase_path, settings)
# TODO: settings serialization for client-server systems
# def serialise_entity_instance(entity):
# return {"cast_type": "entity_instance", "value": entity.id(), "Name": getattr(entity, "Name", None)}
# vcs_settings = settings.copy()
# for key, value in settings.items():
# if isinstance(value, ifcopenshell.entity_instance):
# vcs_settings[key] = serialise_entity_instance(value)
# elif isinstance(value, numpy.ndarray):
# vcs_settings[key] = {"cast_type": "ndarray", "value": value.tolist()}
# elif isinstance(value, list) and value and isinstance(value[0], ifcopenshell.entity_instance):
# vcs_settings[key] = [serialise_entity_instance(i) for i in value]
if "add_representation" in usecase_path:
pass
# print(usecase_path, "{ ... settings too complex right now ... }")
elif "owner." in usecase_path:
pass
else:
pass
# print(vcs_settings)
# try:
# print(usecase_path, json.dumps(vcs_settings))
# except:
# print(usecase_path, vcs_settings)
usecase_class = CACHED_USECASE_CLASSES.get(usecase_path)
if usecase_class is None:
importlib.import_module(f"ifcopenshell.api.{usecase_path}")
@@ -229,11 +256,8 @@ def remove_all_listeners():
def extract_docs(module, usecase):
import typing
import inspect
import collections
results = []
inputs = collections.OrderedDict()
function_init = getattr(getattr(ifcopenshell.api, module), usecase).Usecase.__init__
@@ -275,3 +299,87 @@ def extract_docs(module, usecase):
node_data["description"] = description.strip()
node_data["inputs"] = inputs
return node_data
def serialise_settings(settings):
def serialise_entity_instance(entity):
return {"cast_type": "entity_instance", "value": entity.id(), "Name": getattr(entity, "Name", None)}
vcs_settings = settings.copy()
for key, value in settings.items():
if isinstance(value, ifcopenshell.entity_instance):
vcs_settings[key] = serialise_entity_instance(value)
elif isinstance(value, numpy.ndarray):
vcs_settings[key] = {"cast_type": "ndarray", "value": value.tolist()}
elif isinstance(value, list) and value and isinstance(value[0], ifcopenshell.entity_instance):
vcs_settings[key] = [serialise_entity_instance(i) for i in value]
else:
try:
vcs_settings[key] = str(value)
except:
vcs_settings[key] = "n/a"
try:
return json.dumps(vcs_settings)
except:
return str(vcs_settings)
def wrap_usecase(usecase_path, usecase):
"""Wraps an API function in pre/post listeners."""
def wrapper(*args, should_run_listeners: bool = True, **settings):
ifc_file = args[0] if args else None
nonlocal usecase_path
if should_run_listeners:
listeners = list(pre_listeners.get(usecase_path, {}).values())
listeners += pre_listeners.get("*", {}).values()
for listener in listeners:
listener(usecase_path, ifc_file, settings)
# see #4531
if usecase_path in ARGUMENTS_DEPRECATION:
usecase_path, settings = ARGUMENTS_DEPRECATION[usecase_path](usecase_path, settings)
try:
result = usecase(*args, **settings)
except TypeError as e:
if not e.args[0].startswith(f"{usecase.__name__}()"):
# signature errors typically start with function name
# e.g. "TypeError: edit_library() got an unexpected keyword argument 'test'"
# otherwise it's an error inside api call and we shouldn't get in the way
raise e
msg = (
f"Incorrect function arguments provided for {usecase_path}\n{str(e)}. "
f"You specified args {args} and settings {settings}\n\n"
f"Correct signature is {inspect.signature(usecase)}\n"
f"See help(ifcopenshell.api.{usecase_path}) for documentation."
)
raise TypeError(msg) from e
if should_run_listeners:
listeners = list(post_listeners.get(usecase_path, {}).values())
listeners += post_listeners.get("*", {}).values()
for listener in listeners:
listener(usecase_path, ifc_file, settings)
return result
wrapper.__signature__ = inspect.signature(usecase)
wrapper.__doc__ = usecase.__doc__
wrapper.__name__ = usecase_path
return wrapper
def wrap_usecases(path, name):
"""This developer feature wraps an API module's usecases with listeners."""
import sys
import pkgutil
module_name = name.split(".")[-1]
module = sys.modules[name]
for loader, usecase_name, is_pkg in pkgutil.iter_modules(path):
# We may not be able to get the usecase if we are missing a dependency.
usecase = getattr(module, usecase_name, None)
if callable(usecase):
usecase_path = f"{module_name}.{usecase_name}"
setattr(module, usecase_name, wrap_usecase(usecase_path, usecase))
@@ -16,9 +16,20 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Aggregates are the concept of breaking down larger wholes into smaller parts.
"""Aggregates is the concept of breaking down larger wholes into smaller parts.
One common use is spatial elements, such as how a site has multiple buildings,
and a building has multiple storeys. Another is for regular elements, such as
how a wall is made out of members and coverings.
For example, spatial elements such as sites are broken down into one or more
buildings, and a building is broken down into storeys. Another example is for
physical elements, such as how a wall is made out of members and coverings.
"""
from .. import wrap_usecases
from .assign_object import assign_object
from .unassign_object import unassign_object
wrap_usecases(__path__, __name__)
__all__ = [
"assign_object",
"unassign_object",
]
@@ -18,153 +18,150 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.placement
from typing import Union
class Usecase:
def __init__(
self,
file: ifcopenshell.file,
products: list[ifcopenshell.entity_instance],
relating_object: ifcopenshell.entity_instance,
):
"""Assigns object as an aggregate to the products
def assign_object(
file: ifcopenshell.file,
products: list[ifcopenshell.entity_instance],
relating_object: ifcopenshell.entity_instance,
) -> Union[ifcopenshell.entity_instance, None]:
"""Assigns object as an aggregate to the products
All physical IFC model elements must be part of a hierarchical tree
called the "spatial decomposition", where large things are made up of
smaller things. This tree always begins at an "IfcProject" and is then
broken down using "decomposition" relationships, of which aggregation is
the first relationship you will use.
All physical IFC model elements must be part of a hierarchical tree
called the "spatial decomposition", where large things are made up of
smaller things. This tree always begins at an "IfcProject" and is then
broken down using "decomposition" relationships, of which aggregation is
the first relationship you will use.
Typically used when you want to describe how large spaces are made up of
smaller spaces. For example large spatial elements (e.g. sites,
buidings) can be made out of smaller spatial elements (e.g. storeys,
spaces).
Typically used when you want to describe how large spaces are made up of
smaller spaces. For example large spatial elements (e.g. sites,
buidings) can be made out of smaller spatial elements (e.g. storeys,
spaces).
The largest space (typically the IfcSite) can then be aggregated in a
project. It is requirement for all spatial structures to be directly or
indirectly aggregated back to the IfcProject to create a hierarchy of
spaces.
The largest space (typically the IfcSite) can then be aggregated in a
project. It is requirement for all spatial structures to be directly or
indirectly aggregated back to the IfcProject to create a hierarchy of
spaces.
The other common usecase is when larger physical products are made up of
smaller physical products. For example, a stair might be made out of a
flight, a landing, a railing and so on. Or a wall might be made out of
stud members, and coverings.
The other common usecase is when larger physical products are made up of
smaller physical products. For example, a stair might be made out of a
flight, a landing, a railing and so on. Or a wall might be made out of
stud members, and coverings.
As a product may only have a single location in the "spatial
decomposition" tree, assigning an aggregate relationship will remove any
previous aggregation, containment, or nesting relationships it may have.
As a product may only have a single location in the "spatial
decomposition" tree, assigning an aggregate relationship will remove any
previous aggregation, containment, or nesting relationships it may have.
IFC placements follow a convention where the placement is relative to
its parent in the spatial hierarchy. If your product has a placement,
its placement will be recalculated to follow this convention.
IFC placements follow a convention where the placement is relative to
its parent in the spatial hierarchy. If your product has a placement,
its placement will be recalculated to follow this convention.
:param products: The list of parts of the aggregate, typically of IfcElement or
IfcSpatialStructureElement subclass
:type product: list[ifcopenshell.entity_instance.entity_instance]
:param relating_object: The whole of the aggregate, typically an
IfcElement or IfcSpatialStructureElement subclass
:type relating_object: ifcopenshell.entity_instance.entity_instance
:return: The IfcRelAggregate relationship instance
or `None` if `products` was empty list.
:rtype: Union[ifcopenshell.entity_instance.entity_instance, None]
:param products: The list of parts of the aggregate, typically of IfcElement or
IfcSpatialStructureElement subclass
:type product: list[ifcopenshell.entity_instance]
:param relating_object: The whole of the aggregate, typically an
IfcElement or IfcSpatialStructureElement subclass
:type relating_object: ifcopenshell.entity_instance
:return: The IfcRelAggregate relationship instance
or `None` if `products` was empty list.
:rtype: Union[ifcopenshell.entity_instance, None]
Example:
Example:
.. code:: python
.. code:: python
project = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcProject")
element = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSite")
subelement = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding")
project = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcProject")
element = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSite")
subelement = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding")
# The project contains a site (note that project aggregation is a special case in IFC)
ifcopenshell.api.run("aggregate.assign_object", model, products=[element], relating_object=project)
# The project contains a site (note that project aggregation is a special case in IFC)
ifcopenshell.api.run("aggregate.assign_object", model, products=[element], relating_object=project)
# The site has a building
ifcopenshell.api.run("aggregate.assign_object", model, products=[subelement], relating_object=element)
"""
self.file = file
self.settings = {
"products": products,
"relating_object": relating_object,
}
# The site has a building
ifcopenshell.api.run("aggregate.assign_object", model, products=[subelement], relating_object=element)
"""
settings = {
"products": products,
"relating_object": relating_object,
}
def execute(self) -> Union[ifcopenshell.entity_instance, None]:
if not self.settings["products"]:
return
if not settings["products"]:
return
products = set(self.settings["products"])
relating_object = self.settings["relating_object"]
is_decomposed_by = next((i for i in relating_object.IsDecomposedBy if i.is_a("IfcRelAggregates")), None)
products = set(settings["products"])
relating_object = settings["relating_object"]
is_decomposed_by = next((i for i in relating_object.IsDecomposedBy if i.is_a("IfcRelAggregates")), None)
previous_aggregates_rels: set[ifcopenshell.entity_instance] = set()
products_without_aggregates: list[ifcopenshell.entity_instance] = []
products_with_aggregates: list[ifcopenshell.entity_instance] = []
previous_aggregates_rels: set[ifcopenshell.entity_instance] = set()
products_without_aggregates: list[ifcopenshell.entity_instance] = []
products_with_aggregates: list[ifcopenshell.entity_instance] = []
# check if there is anything to change
for product in products:
product_rel = next(iter(product.Decomposes), None)
# check if there is anything to change
for product in products:
product_rel = next(iter(product.Decomposes), None)
if product_rel is None:
products_without_aggregates.append(product)
continue
if product_rel is None:
products_without_aggregates.append(product)
continue
# either is_decomposed_by is None or product is part of different rel
if product_rel != is_decomposed_by:
previous_aggregates_rels.add(product_rel)
products_with_aggregates.append(product)
# either is_decomposed_by is None or product is part of different rel
if product_rel != is_decomposed_by:
previous_aggregates_rels.add(product_rel)
products_with_aggregates.append(product)
# products with already assigned aggregates will be skipped
# products with already assigned aggregates will be skipped
products_to_change = products_without_aggregates + products_with_aggregates
# nothing to change
if not products_to_change:
return is_decomposed_by
products_to_change = products_without_aggregates + products_with_aggregates
# nothing to change
if not products_to_change:
return is_decomposed_by
# can be either only aggregated or only contained at the same time
# some product might not be able to have a container
possibly_contained_products = [p for p in products_without_aggregates if hasattr(p, "ContainedInStructure")]
ifcopenshell.api.run("spatial.unassign_container", self.file, products=possibly_contained_products)
# can be either only aggregated or only contained at the same time
# some product might not be able to have a container
possibly_contained_products = [p for p in products_without_aggregates if hasattr(p, "ContainedInStructure")]
ifcopenshell.api.run("spatial.unassign_container", file, products=possibly_contained_products)
# unassign elements from previous aggregates
for decomposes in previous_aggregates_rels:
related_objects = set(decomposes.RelatedObjects) - products
if related_objects:
decomposes.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": decomposes})
else:
history = decomposes.OwnerHistory
self.file.remove(decomposes)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
# assign elements to a new aggregate
if is_decomposed_by:
is_decomposed_by.RelatedObjects = list(set(is_decomposed_by.RelatedObjects) | products)
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": is_decomposed_by})
# unassign elements from previous aggregates
for decomposes in previous_aggregates_rels:
related_objects = set(decomposes.RelatedObjects) - products
if related_objects:
decomposes.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", file, **{"element": decomposes})
else:
is_decomposed_by = self.file.create_entity(
"IfcRelAggregates",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
"RelatedObjects": list(products),
"RelatingObject": relating_object,
}
history = decomposes.OwnerHistory
file.remove(decomposes)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
# assign elements to a new aggregate
if is_decomposed_by:
is_decomposed_by.RelatedObjects = list(set(is_decomposed_by.RelatedObjects) | products)
ifcopenshell.api.run("owner.update_owner_history", file, **{"element": is_decomposed_by})
else:
is_decomposed_by = file.create_entity(
"IfcRelAggregates",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", file),
"RelatedObjects": list(products),
"RelatingObject": relating_object,
}
)
# localize placement relative to a new aggregate for affected products
for product in products_to_change:
placement = getattr(product, "ObjectPlacement", None)
if placement and placement.is_a("IfcLocalPlacement"):
ifcopenshell.api.run(
"geometry.edit_object_placement",
file,
product=product,
matrix=ifcopenshell.util.placement.get_local_placement(product.ObjectPlacement),
is_si=False,
)
# localize placement relative to a new aggregate for affected products
for product in products_to_change:
placement = getattr(product, "ObjectPlacement", None)
if placement and placement.is_a("IfcLocalPlacement"):
ifcopenshell.api.run(
"geometry.edit_object_placement",
self.file,
product=product,
matrix=ifcopenshell.util.placement.get_local_placement(product.ObjectPlacement),
is_si=False,
)
return is_decomposed_by
return is_decomposed_by
@@ -21,60 +21,57 @@ import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
def __init__(self, file: ifcopenshell.file, products: list[ifcopenshell.entity_instance]):
"""Unassigns products from their aggregate
def unassign_object(file: ifcopenshell.file, products: list[ifcopenshell.entity_instance]) -> None:
"""Unassigns products from their aggregate
A product (i.e. a smaller part of a whole) may be aggregated into zero
or one larger space or element. This function will remove that
aggregation relationship.
A product (i.e. a smaller part of a whole) may be aggregated into zero
or one larger space or element. This function will remove that
aggregation relationship.
As all physical IFC model elements must be part of a hierarchical tree
called the "spatial decomposition", using this function will remove the
product from that tree. This is a dangerous operation and may result in
the product no longer being visible in IFC applications.
As all physical IFC model elements must be part of a hierarchical tree
called the "spatial decomposition", using this function will remove the
product from that tree. This is a dangerous operation and may result in
the product no longer being visible in IFC applications.
If the product is not part of an aggregation relationship, nothing will
happen.
If the product is not part of an aggregation relationship, nothing will
happen.
:param products: The list of parts of the aggregate, typically of IfcElements or
IfcSpatialStructureElement subclass
:type product: list[ifcopenshell.entity_instance.entity_instance]
:return: None
:rtype: None
:param products: The list of parts of the aggregate, typically of IfcElements or
IfcSpatialStructureElement subclass
:type product: list[ifcopenshell.entity_instance]
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
element = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSite")
subelement1 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding")
subelement2 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding")
ifcopenshell.api.run("aggregate.assign_object", model, products=[subelement1], relating_object=element)
ifcopenshell.api.run("aggregate.assign_object", model, products=[subelement2], relating_object=element)
# nothing is returned
ifcopenshell.api.run("aggregate.unassign_object", model, products=[subelement1])
# nothing is returned, relationship is removed
ifcopenshell.api.run("aggregate.unassign_object", model, products=[subelement2])
"""
self.file = file
self.settings = {"products": products}
element = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSite")
subelement1 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding")
subelement2 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding")
ifcopenshell.api.run("aggregate.assign_object", model, products=[subelement1], relating_object=element)
ifcopenshell.api.run("aggregate.assign_object", model, products=[subelement2], relating_object=element)
# nothing is returned
ifcopenshell.api.run("aggregate.unassign_object", model, products=[subelement1])
# nothing is returned, relationship is removed
ifcopenshell.api.run("aggregate.unassign_object", model, products=[subelement2])
"""
settings = {"products": products}
def execute(self) -> None:
products = set(self.settings["products"])
rels = set(
rel
for product in products
if (rel := next((rel for rel in product.Decomposes if rel.is_a("IfcRelAggregates")), None))
)
products = set(settings["products"])
rels = set(
rel
for product in products
if (rel := next((rel for rel in product.Decomposes if rel.is_a("IfcRelAggregates")), None))
)
for rel in rels:
related_objects = set(rel.RelatedObjects) - products
if related_objects:
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
else:
history = rel.OwnerHistory
self.file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
for rel in rels:
related_objects = set(rel.RelatedObjects) - products
if related_objects:
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", file, **{"element": rel})
else:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
@@ -15,3 +15,19 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Basic modification of the attributes of an element.
All IFC entities have attributes. Some of these attributes contain rules about
inheritance and what they are allowed to contain. These usecases make sure that
any editing complies with these rules.
"""
from .. import wrap_usecases
from .edit_attributes import edit_attributes
wrap_usecases(__path__, __name__)
__all__ = [
"edit_attributes",
]
@@ -17,66 +17,53 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api
import ifcopenshell.util.element
from typing import Any
class Usecase:
def __init__(self, file, product=None, attributes=None):
"""Edit the attributes of a product
def edit_attributes(file: ifcopenshell.file, product: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
"""Edit the attributes of a product
All IFC entities have attributes. Normally they can be edited directly,
by simply assigning a new value to them. In some scenarios, you may wish
to also ensure that ownership history is updated. This function provides
that convenience.
All IFC entities have attributes. Normally they can be edited directly,
by simply assigning a new value to them. In some scenarios, you may wish
to also ensure that ownership history is updated. This function provides
that convenience.
:param product: The product you want to edit. This may be any rooted IFC
entity.
:type product: ifcopenshell.entity_instance.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:return: None
:rtype: None
:param product: The product you want to edit. This may be any rooted IFC
entity.
:type product: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
element = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall")
ifcopenshell.api.run("attribute.edit_attributes", model,
product=element, attributes={"Name": "Waldo"})
"""
self.file = file
self.settings = {"product": product, "attributes": attributes or {}}
element = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall")
ifcopenshell.api.run("attribute.edit_attributes", model,
product=element, attributes={"Name": "Waldo"})
"""
settings = {"product": product, "attributes": attributes or {}}
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["product"], name, value)
if hasattr(self.settings["product"], "PredefinedType"):
if hasattr(self.settings["product"], "ElementType"):
if (
self.settings["product"].ElementType is None
and self.settings["product"].PredefinedType == "USERDEFINED"
):
self.settings["product"].PredefinedType = "NOTDEFINED"
elif (
self.settings["product"].ElementType
and self.settings["product"].PredefinedType != "USERDEFINED"
):
self.settings["product"].PredefinedType = "USERDEFINED"
elif hasattr(self.settings["product"], "ObjectType"):
relating_type = ifcopenshell.util.element.get_type(self.settings["product"])
# Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818
if relating_type and relating_type.PredefinedType not in ("NOTDEFINED", None):
self.settings["product"].ObjectType = None
self.settings["product"].PredefinedType = None
elif (
self.settings["product"].ObjectType is None
and self.settings["product"].PredefinedType == "USERDEFINED"
):
self.settings["product"].PredefinedType = "NOTDEFINED"
elif (
self.settings["product"].ObjectType
and self.settings["product"].PredefinedType != "USERDEFINED"
):
self.settings["product"].PredefinedType = "USERDEFINED"
if hasattr(self.settings["product"], "OwnerHistory"):
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": self.settings["product"]})
for name, value in settings["attributes"].items():
setattr(settings["product"], name, value)
if hasattr(settings["product"], "PredefinedType"):
if hasattr(settings["product"], "ElementType"):
if settings["product"].ElementType is None and settings["product"].PredefinedType == "USERDEFINED":
settings["product"].PredefinedType = "NOTDEFINED"
elif settings["product"].ElementType and settings["product"].PredefinedType != "USERDEFINED":
settings["product"].PredefinedType = "USERDEFINED"
elif hasattr(settings["product"], "ObjectType"):
relating_type = ifcopenshell.util.element.get_type(settings["product"])
# Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818
if relating_type and relating_type.PredefinedType not in ("NOTDEFINED", None):
settings["product"].ObjectType = None
settings["product"].PredefinedType = None
elif settings["product"].ObjectType is None and settings["product"].PredefinedType == "USERDEFINED":
settings["product"].PredefinedType = "NOTDEFINED"
elif settings["product"].ObjectType and settings["product"].PredefinedType != "USERDEFINED":
settings["product"].PredefinedType = "USERDEFINED"
if hasattr(settings["product"], "OwnerHistory"):
ifcopenshell.api.run("owner.update_owner_history", file, **{"element": settings["product"]})
@@ -18,4 +18,22 @@
"""Boundaries are primarily used for representing virtual interfaces between
spaces for energy analysis.
Boundaries may be associated with spaces or physical elements that enclose
spaces such as walls, doors, and windows.
"""
from .. import wrap_usecases
from .assign_connection_geometry import assign_connection_geometry
from .copy_boundary import copy_boundary
from .edit_attributes import edit_attributes
from .remove_boundary import remove_boundary
wrap_usecases(__path__, __name__)
__all__ = [
"assign_connection_geometry",
"copy_boundary",
"edit_attributes",
"remove_boundary",
]
@@ -17,70 +17,82 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.unit
from typing import Optional
def assign_connection_geometry(
file: ifcopenshell.file,
rel_space_boundary: ifcopenshell.entity_instance,
outer_boundary: list[tuple[float, float]],
location: tuple[float, float, float],
axis: tuple[float, float, float],
ref_direction: tuple[float, float, float],
inner_boundaries: Optional[list[list[tuple[float, float]]]] = None,
unit_scale: Optional[float] = None,
) -> None:
"""Create and assign a connection geometry to a space boundary relationship
A space boundary may optionally have a plane that represents how that
space is adjacent to another space, known as the connection geometry.
You may specify this plane in terms of an outer boundary polyline, zero
or more inner boundaries (such as for windows), and a positional matrix
for the orientation of the plane.
:param rel_space_boundary: The space boundary relationship to assign the
connection geometry to.
:type rel_space_boundary: ifcopenshell.entity_instance
:param outer_boundary: A list of 2D points representing an open
polyline. The last point will connect to the first point. Each
point is represented by an interable of 2 floats. The coordinates of
the points are relative to the positional matrix arguments.
:type outer_boundary: list[tuple[float, float]]
:param inner_boundaries: A list of zero or more inner boundaries to use
for the plane. Each boundary is represented by an open polyline, as
defined by the outer_boundary argument.
:type inner_boundaries: list[list[tuple[float, float]]], optional
:param location: The local origin of the connection geometry, defined as
an XYZ coordinate relative to the placement of the space that is
being bounded.
:type location: tuple[float, float, float]
:param axis: The local X axis of the connection geometry, defined as an
XYZ vector relative to the placement of the space that is being
bounded.
:type axis: tuple[float, float, float]
:param ref_direction: The local Z axis of the connection geometry,
defined as an XYZ vector relative to the placement of the space that
is being bounded. The Y vector is automatically derived using the
right hand rule.
:type ref_direction: tuple[float, float, float]
:param unit_scale: The unit scale as calculated by
ifcopenshell.util.unit.calculate_unit_scale. If not provided, it
will be automatically calculated for you.
:type unit_scale: float, optional
:return: None
:rtype: None
Example:
.. code:: python
ifcopenshell.api.run("boundary.assign_connection_geometry", model,
rel_space_boundary=element,
outer_boundary=[(0., 0.), (1., 0.), (1., 1.), (0., 1.)],
location=[0., 0., 0.], axis=[1., 0., 0.], ref_direction=[0., 0., 1.],
)
"""
usecase = Usecase()
usecase.file = file
usecase.rel_space_boundary = rel_space_boundary
usecase.outer_boundary = outer_boundary
usecase.inner_boundaries = inner_boundaries or ()
usecase.location = location
usecase.axis = axis
usecase.ref_direction = ref_direction
usecase.unit_scale = unit_scale
return usecase.execute()
class Usecase:
def __init__(self, file, rel_space_boundary=None, outer_boundary=None, inner_boundaries=None, location=None, axis=None, ref_direction=None, unit_scale=None):
"""Create and assign a connection geometry to a space boundary relationship
A space boundary may optionally have a plane that represents how that
space is adjacent to another space, known as the connection geometry.
You may specify this plane in terms of an outer boundary polyline, zero
or more inner boundaries (such as for windows), and a positional matrix
for the orientation of the plane.
:param rel_space_boundary: The space boundary relationship to assign the
connection geometry to.
:type rel_space_boundary: ifcopenshell.entity_instance.entity_instance
:param outer_boundary: A list of 2D points representing an open
polyline. The last point will connect to the first point. Each
point is represented by an interable of 2 floats. The coordinates of
the points are relative to the positional matrix arguments.
:type outer_boundary: list[list[float]]
:param inner_boundaries: A list of zero or more inner boundaries to use
for the plane. Each boundary is represented by an open polyline, as
defined by the outer_boundary argument.
:type inner_boundaries: list[list[list[float]]], optional
:param location: The local origin of the connection geometry, defined as
an XYZ coordinate relative to the placement of the space that is
being bounded.
:type location: list[float]
:param axis: The local X axis of the connection geometry, defined as an
XYZ vector relative to the placement of the space that is being
bounded.
:type axis: list[float]
:param ref_direction: The local Z axis of the connection geometry,
defined as an XYZ vector relative to the placement of the space that
is being bounded. The Y vector is automatically derived using the
right hand rule.
:type ref_direction: list[float]
:param unit_scale: The unit scale as calculated by
ifcopenshell.util.unit.calculate_unit_scale. If not provided, it
will be automatically calculated for you.
:type unit_scale: float, optional
:return: None
:rtype: None
Example:
.. code:: python
ifcopenshell.api.run("boundary.assign_connection_geometry", model,
rel_space_boundary=element,
outer_boundary=[(0., 0.), (1., 0.), (1., 1.), (0., 1.)],
location=[0., 0., 0.], axis=[1., 0., 0.], ref_direction=[0., 0., 1.],
)
"""
self.file = file
self.rel_space_boundary = rel_space_boundary
self.outer_boundary = outer_boundary
self.inner_boundaries = inner_boundaries or ()
self.location = location
self.axis = axis
self.ref_direction = ref_direction
self.unit_scale = unit_scale
self.ifc_vertices = []
def execute(self):
if self.unit_scale is None:
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -19,29 +19,26 @@
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, boundary=None):
"""Copies a space boundary
def copy_boundary(file: ifcopenshell.file, boundary: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
"""Copies a space boundary
:param boundary: The IfcRelSpaceBoundary you want to copy.
:type boundary: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param boundary: The IfcRelSpaceBoundary you want to copy.
:type boundary: ifcopenshell.entity_instance
:return: Duplicate of the IfcRelSpaceBoundary
:rtype: ifcopenshell.entity_instance
Example:
Example:
# A boring boundary with no geometry. Note that this boundary is
# invalid and does not relate to any space or building element.
boundary = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcRelSpaceBoundary")
# A boring boundary with no geometry. Note that this boundary is
# invalid and does not relate to any space or building element.
boundary = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcRelSpaceBoundary")
# And now we have two
boundary_copy = ifcopenshell.api.run("boundary.copy_boundary", model, boundary=boundary)
"""
self.file = file
self.settings = {"boundary": boundary}
# And now we have two
boundary_copy = ifcopenshell.api.run("boundary.copy_boundary", model, boundary=boundary)
"""
settings = {"boundary": boundary}
def execute(self):
result = ifcopenshell.util.element.copy(self.file, self.settings["boundary"])
if result.ConnectionGeometry:
result.ConnectionGeometry = ifcopenshell.util.element.copy_deep(self.file, result.ConnectionGeometry)
return result
result = ifcopenshell.util.element.copy(file, settings["boundary"])
if result.ConnectionGeometry:
result.ConnectionGeometry = ifcopenshell.util.element.copy_deep(file, result.ConnectionGeometry)
return result
@@ -15,47 +15,53 @@
#
# 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
from typing import Optional
class Usecase:
def __init__(self, file, entity=None, relating_space=None, related_building_element=None, parent_boundary=None, corresponding_boundary=None):
"""Modify the relationships of a space boundary relationship
def edit_attributes(
file: ifcopenshell.file,
entity: ifcopenshell.entity_instance,
relating_space: ifcopenshell.entity_instance,
related_building_element: ifcopenshell.entity_instance,
parent_boundary: Optional[ifcopenshell.entity_instance] = None,
corresponding_boundary: Optional[ifcopenshell.entity_instance] = None,
) -> None:
"""Modify the relationships of a space boundary relationship
Currently this function is quite minimal and offers no advantage to
manual assignment of the space boundary attributes.
Currently this function is quite minimal and offers no advantage to
manual assignment of the space boundary attributes.
:param entity: The IfcRelSpaceBoundary to modify
:type entity: ifcopenshell.entity_instance.entity_instance
:param relating_space: The IfcSpace or IfcExternalSpatialElement that
the space boundary is related to.
:type relating_space: ifcopenshell.entity_instance.entity_instance
:param related_building_element: The IfcElement that defines the
boundary, typically an IfcWall.
:type relating_space: ifcopenshell.entity_instance.entity_instance
:param parent_boundary: A parent IfcRelSpaceBoundary, only provided if
this is an inner boundary. This can apply to 1st and 2nd level
boundaries.
:type parent_boundary: ifcopenshell.entity_instance.entity_instance,
optional
:param corresponding_boundary: The other IfcRelSpaceBoundary on the
other side of the related element. The pair together represents a
thermal boundary. This only applies to 2nd level boundaries.
:type corresponding_boundary: ifcopenshell.entity_instance.entity_instance,
optional
:return: None
:rtype: None
"""
self.file = file
self.entity = entity
self.relating_space = relating_space
self.related_building_element = related_building_element
self.parent_boundary = parent_boundary
self.corresponding_boundary = corresponding_boundary
:param entity: The IfcRelSpaceBoundary to modify
:type entity: ifcopenshell.entity_instance
:param relating_space: The IfcSpace or IfcExternalSpatialElement that
the space boundary is related to.
:type relating_space: ifcopenshell.entity_instance
:param related_building_element: The IfcElement that defines the
boundary, typically an IfcWall.
:type relating_space: ifcopenshell.entity_instance
:param parent_boundary: A parent IfcRelSpaceBoundary, only provided if
this is an inner boundary. This can apply to 1st and 2nd level
boundaries.
:type parent_boundary: ifcopenshell.entity_instance,
optional
:param corresponding_boundary: The other IfcRelSpaceBoundary on the
other side of the related element. The pair together represents a
thermal boundary. This only applies to 2nd level boundaries.
:type corresponding_boundary: ifcopenshell.entity_instance,
optional
:return: None
:rtype: None
"""
entity = entity
relating_space = relating_space
related_building_element = related_building_element
parent_boundary = parent_boundary
corresponding_boundary = corresponding_boundary
def execute(self):
self.entity.RelatingSpace = self.relating_space
self.entity.RelatedBuildingElement = self.related_building_element
if hasattr(self.entity, "ParentBoundary"):
self.entity.ParentBoundary = self.parent_boundary
if hasattr(self.entity, "CorrespondingBoundary"):
self.entity.CorrespondingBoundary = self.corresponding_boundary
entity.RelatingSpace = relating_space
entity.RelatedBuildingElement = related_building_element
if hasattr(entity, "ParentBoundary"):
entity.ParentBoundary = parent_boundary
if hasattr(entity, "CorrespondingBoundary"):
entity.CorrespondingBoundary = corresponding_boundary
@@ -20,36 +20,33 @@ import ifcopenshell
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, boundary=None):
"""Removes a space boundary
def remove_boundary(file: ifcopenshell.file, boundary: ifcopenshell.entity_instance) -> None:
"""Removes a space boundary
The relating space or related building element is untouched. Only the
boundary and its connection geometry is removed.
The relating space or related building element is untouched. Only the
boundary and its connection geometry is removed.
:param boundary: The IfcRelSpaceBoundary you want to remove.
:type boundary: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param boundary: The IfcRelSpaceBoundary you want to remove.
:type boundary: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
Example:
# A boring boundary with no geometry. Note that this boundary is
# invalid and does not relate to any space or building element.
boundary = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcRelSpaceBoundary")
# A boring boundary with no geometry. Note that this boundary is
# invalid and does not relate to any space or building element.
boundary = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcRelSpaceBoundary")
# Let's remove it!
ifcopenshell.api.run("boundary.remove_boundary", model, boundary=boundary)
"""
self.file = file
self.settings = {"boundary": boundary}
# Let's remove it!
ifcopenshell.api.run("boundary.remove_boundary", model, boundary=boundary)
"""
settings = {"boundary": boundary}
def execute(self):
geometry = self.settings["boundary"].ConnectionGeometry
if geometry:
self.settings["boundary"].ConnectionGeometry = None
ifcopenshell.util.element.remove_deep2(self.file, geometry)
history = self.settings["boundary"].OwnerHistory
self.file.remove(self.settings["boundary"])
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
geometry = settings["boundary"].ConnectionGeometry
if geometry:
settings["boundary"].ConnectionGeometry = None
ifcopenshell.util.element.remove_deep2(file, geometry)
history = settings["boundary"].OwnerHistory
file.remove(settings["boundary"])
if history:
ifcopenshell.util.element.remove_deep2(file, history)
@@ -15,3 +15,33 @@
#
# 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",
]
@@ -17,72 +17,78 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.guid
import ifcopenshell.util.schema
import ifcopenshell.util.date
from typing import Union
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.
:type classification: str,ifcopenshell.entity_instance
:return: The added IfcClassification element
:rtype: ifcopenshell.entity_instance
Example:
.. code:: python
# Option 1: adding a custom clasification from scratch
ifcopenshell.api.run("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.run("classification.add_classification", model,
classification=classification)
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"classification": classification,
}
return usecase.execute()
class Usecase:
def __init__(self, file: ifcopenshell.file, classification: Union[str, 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.
:type classification: str,ifcopenshell.entity_instance.entity_instance
:return: The added IfcClassification element
:rtype: ifcopenshell.entity_instance.entity_instance
Example:
.. code:: python
# Option 1: adding a custom clasification from scratch
ifcopenshell.api.run("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.run("classification.add_classification", model,
classification=classification)
"""
self.file = file
self.settings = {
"classification": classification,
}
def execute(self) -> ifcopenshell.entity_instance:
def execute(self):
if isinstance(self.settings["classification"], str):
classification = self.file.createIfcClassification(Name=self.settings["classification"])
self.relate_to_project(classification)
@@ -104,7 +110,9 @@ class Usecase:
"IfcCalendarDate", **ifcopenshell.util.date.datetime2ifc(edition_date, "IfcCalendarDate")
)
else:
result.EditionDate = ifcopenshell.util.date.datetime2ifc(edition_date, "IfcDate")
if edition_date:
edition_date = ifcopenshell.util.date.datetime2ifc(edition_date, "IfcDate")
result.EditionDate = edition_date
self.relate_to_project(result)
@@ -18,122 +18,125 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.schema
from typing import Optional, Union
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.
:type product: list[ifcopenshell.entity_instance]
:param reference: The classification reference entity taken from an
IFC classification library. If you supply this parameter, you will
use option 2.
:type reference: ifcopenshell.entity_instance, optional
: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).
:type identification: str, optional
: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.
:type name: str, optional
:param classification: The IfcClassification entity in your IFC model
(not the library, if you are doing option 2) that the reference is
part of.
:type classification: ifcopenshell.entity_instance
: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.
:type is_lightweight: bool, optional
:raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements.
:return: The newly added IfcClassificationReference
or `None` if `products` was empty list.
:rtype: Union[ifcopenshell.entity_instance, None]
Example:
.. code:: python
# Option 1: adding and assigning a new reference from scratch
wall_type = model.by_type("IfcWallType")[0]
classification = ifcopenshell.api.run("classification.add_classification",
model, classification="MyCustomClassification")
ifcopenshell.api.run("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.run("classification.add_classification",
model, classification=lib_classification)
reference = [r for r in library.by_type("IfcClassificationReference")
if r.Identification == "XYZ"][0]
ifcopenshell.api.run("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:
def __init__(
self,
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,
):
"""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.
:type product: list[ifcopenshell.entity_instance.entity_instance]
:param reference: The classification reference entity taken from an
IFC classification library. If you supply this parameter, you will
use option 2.
:type reference: ifcopenshell.entity_instance.entity_instance, optional
: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).
:type identification: str, optional
: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.
:type name: str, optional
:param classification: The IfcClassification entity in your IFC model
(not the library, if you are doing option 2) that the reference is
part of.
:type classification: ifcopenshell.entity_instance.entity_instance
: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.
:type is_lightweight: bool, optional
:raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements.
:return: The newly added IfcClassificationReference
or `None` if `products` was empty list.
:rtype: Union[ifcopenshell.entity_instance.entity_instance, None]
Example:
.. code:: python
# Option 1: adding and assigning a new reference from scratch
wall_type = model.by_type("IfcWallType")[0]
classification = ifcopenshell.api.run("classification.add_classification",
model, classification="MyCustomClassification")
ifcopenshell.api.run("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.run("classification.add_classification",
model, classification=lib_classification)
reference = [r for r in library.by_type("IfcClassificationReference")
if r.Identification == "XYZ"][0]
ifcopenshell.api.run("classification.add_reference", model,
products=[wall_type], classification=classification,
reference=reference)
"""
self.file = file
self.settings = {
"products": products,
"reference": reference,
"identification": identification,
"name": name,
"classification": classification,
"is_lightweight": is_lightweight,
}
def execute(self) -> Union[ifcopenshell.entity_instance, None]:
def execute(self):
if not self.settings["products"]:
return
@@ -15,34 +15,35 @@
#
# 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
from typing import Any
class Usecase:
def __init__(self, file, classification=None, attributes=None):
"""Edits the attributes of an IfcClassification
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.
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
:type classification: ifcopenshell.entity_instance.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:return: None
:rtype: None
:param classification: The IfcClassification entity you want to edit
:type classification: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
classification = model.by_type("IfcClassification")[0]
# Change the name of the classification system to "Foo"
ifcopenshell.api.run("classification.edit_classification", model,
classification=classification, attributes={"Name": "Foo"})
"""
self.file = file
self.settings = {"classification": classification, "attributes": attributes or {}}
classification = model.by_type("IfcClassification")[0]
# Change the name of the classification system to "Foo"
ifcopenshell.api.run("classification.edit_classification", model,
classification=classification, attributes={"Name": "Foo"})
"""
settings = {"classification": classification, "attributes": attributes or {}}
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["classification"], name, value)
for name, value in settings["attributes"].items():
setattr(settings["classification"], name, value)
@@ -15,34 +15,35 @@
#
# 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
from typing import Any
class Usecase:
def __init__(self, file, reference=None, attributes=None):
"""Edits the attributes of an IfcClassificationReference
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.
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
:type reference: ifcopenshell.entity_instance.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:return: None
:rtype: None
:param reference: The IfcClassificationReference entity you want to edit
:type reference: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
reference = model.by_type("IfcClassification")[0]
# Change the name of the reference to "Foo"
ifcopenshell.api.run("classification.edit_reference", model,
reference=reference, attributes={"Name": "Foo"})
"""
self.file = file
self.settings = {"reference": reference, "attributes": attributes or {}}
reference = model.by_type("IfcClassification")[0]
# Change the name of the reference to "Foo"
ifcopenshell.api.run("classification.edit_reference", model,
reference=reference, attributes={"Name": "Foo"})
"""
settings = {"reference": reference, "attributes": attributes or {}}
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["reference"], name, value)
for name, value in settings["attributes"].items():
setattr(settings["reference"], name, value)
@@ -20,30 +20,33 @@ 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
:type classification: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
.. code:: python
classification = model.by_type("IfcClassification")[0]
ifcopenshell.api.run("classification.remove_classification", model,
classification=classification)
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {"classification": classification}
return usecase.execute()
class Usecase:
def __init__(self, file, classification=None):
"""Removes an IfcClassification from the project and all references
The classification and all of its relationships, children references,
and relationships between objectse and child references are completely
removed from a project.
:param classification: The IfcClassification entity you want to remove
:type classification: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
Example:
.. code:: python
classification = model.by_type("IfcClassification")[0]
ifcopenshell.api.run("classification.remove_classification", model,
classification=classification)
"""
self.file = file
self.settings = {"classification": classification}
def execute(self):
references = self.get_references(self.settings["classification"])
for reference in references:
@@ -55,15 +58,20 @@ class Usecase:
self.file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
for rel in self.file.by_type("IfcExternalReferenceRelationship"):
if not rel.RelatingReference:
self.file.remove(rel)
def get_references(self, classification):
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 not classification.HasReferences:
return results
for reference in classification.HasReferences:
results.append(reference)
results.extend(self.get_references(reference))
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
@@ -21,107 +21,102 @@ import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
def __init__(
self,
file: ifcopenshell.file,
reference: ifcopenshell.entity_instance,
products: list[ifcopenshell.entity_instance],
):
"""Removes a classification reference from the list of products
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.
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.
:type reference: ifcopenshell.entity_instance.entity_instance
:param product: The list fo object entities of the relationship you want to
remove.
:type product: list[ifcopenshell.entity_instance.entity_instance]
:param reference: The IfcClassificationReference entity of the
relationship you want to remove.
:type reference: ifcopenshell.entity_instance
:param product: The list fo object entities of the relationship you want to
remove.
:type product: list[ifcopenshell.entity_instance]
:raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements.
:raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements.
:return: None
:rtype: None
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
wall_type = model.by_type("IfcWallType")[0]
classification = ifcopenshell.api.run("classification.add_classification",
model, classification="MyCustomClassification")
reference = ifcopenshell.api.run("classification.add_reference", model,
products=[wall_type], classification=classification,
identification="W_01", name="Interior Walls")
ifcopenshell.api.run("classification.remove_reference", model,
reference=reference, products=[wall_type])
"""
self.file = file
self.settings = {"reference": reference, "products": products}
wall_type = model.by_type("IfcWallType")[0]
classification = ifcopenshell.api.run("classification.add_classification",
model, classification="MyCustomClassification")
reference = ifcopenshell.api.run("classification.add_reference", model,
products=[wall_type], classification=classification,
identification="W_01", name="Interior Walls")
ifcopenshell.api.run("classification.remove_reference", model,
reference=reference, products=[wall_type])
"""
settings = {"reference": reference, "products": products}
def execute(self) -> None:
is_ifc2x3 = self.file.schema == "IFC2X3"
products = set(self.settings["products"])
referenced = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"])
products -= products.difference(referenced)
is_ifc2x3 = file.schema == "IFC2X3"
products = set(settings["products"])
referenced = ifcopenshell.util.element.get_referenced_elements(settings["reference"])
products -= products.difference(referenced)
# all products are already unassigned from a reference
if not products:
return
# all products are already unassigned from a reference
if not products:
return
rooted_products: set[ifcopenshell.entity_instance] = set()
non_rooted_products: set[ifcopenshell.entity_instance] = set()
for product in self.settings["products"]:
if product.is_a("IfcRoot"):
rooted_products.add(product)
rooted_products: set[ifcopenshell.entity_instance] = set()
non_rooted_products: set[ifcopenshell.entity_instance] = set()
for product in settings["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 == settings["reference"]
}
for rel in reference_rels:
related_objects = set(rel.RelatedObjects) - rooted_products
if related_objects:
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", file, **{"element": rel})
else:
non_rooted_products.add(product)
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
if non_rooted_products and is_ifc2x3:
raise TypeError(f"Cannot add reference to non-IfcRoot element in IFC2X3: {non_rooted_products}.")
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)
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.RelatingReference == settings["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)
reference_rels = {
rel
for rel in reference_rels
if rel.is_a("IfcRelAssociatesClassification")
and rel.RelatingClassification == self.settings["reference"]
}
for rel in reference_rels:
related_objects = set(rel.RelatedObjects) - rooted_products
if related_objects:
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
else:
history = rel.OwnerHistory
self.file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(self.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 == self.settings["reference"]}
for rel in reference_rels:
related_objects = set(rel.RelatedResourceObjects) - non_rooted_products
if related_objects:
rel.RelatedResourceObjects = list(related_objects)
else:
self.file.remove(rel)
# TODO: we only handle lightweight classifications here
referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"])
if not referenced_elements:
self.file.remove(self.settings["reference"])
# TODO: we only handle lightweight classifications here
referenced_elements = ifcopenshell.util.element.get_referenced_elements(settings["reference"])
if not referenced_elements:
file.remove(settings["reference"])
@@ -15,3 +15,34 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Constraints are an advanced feature allowing you to specify parametric
limits on properties
Warning: usage of constraints are mostly untested in real life applications.
"""
from .. import wrap_usecases
from .add_metric import add_metric
from .add_metric_reference import add_metric_reference
from .add_objective import add_objective
from .assign_constraint import assign_constraint
from .edit_metric import edit_metric
from .edit_objective import edit_objective
from .remove_constraint import remove_constraint
from .remove_metric import remove_metric
from .unassign_constraint import unassign_constraint
wrap_usecases(__path__, __name__)
__all__ = [
"add_metric",
"add_metric_reference",
"add_objective",
"assign_constraint",
"edit_metric",
"edit_objective",
"remove_constraint",
"remove_metric",
"unassign_constraint",
]
@@ -19,44 +19,41 @@
import ifcopenshell
class Usecase:
def __init__(self, file, objective=None):
"""Add a new metric benchmark
def add_metric(file: ifcopenshell.file, objective: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
"""Add a new metric benchmark
Qualitative constraints may have a series of quantitative benchmarks
linked to it known as metrics. Metrics may be parametrically linked to
computed model properties or quantities. Metrics need to be satisfied
to meet the objective of the constraint.
Qualitative constraints may have a series of quantitative benchmarks
linked to it known as metrics. Metrics may be parametrically linked to
computed model properties or quantities. Metrics need to be satisfied
to meet the objective of the constraint.
:param objective: The IfcObjective that this metric is a benchmark of.
:type objective: ifcopenshell.entity_instance.entity_instance
:return: The newly created IfcMetric entity
:rtype: ifcopenshell.entity_instance.entity_instance
:param objective: The IfcObjective that this metric is a benchmark of.
:type objective: ifcopenshell.entity_instance
:return: The newly created IfcMetric entity
:rtype: ifcopenshell.entity_instance
Example:
Example:
.. code:: python
.. code:: python
objective = ifcopenshell.api.run("constraint.add_objective", model)
metric = ifcopenshell.api.run("constraint.add_metric", model,
objective=objective)
"""
self.file = file
self.settings = {
"objective": objective,
}
objective = ifcopenshell.api.run("constraint.add_objective", model)
metric = ifcopenshell.api.run("constraint.add_metric", model,
objective=objective)
"""
settings = {
"objective": objective,
}
def execute(self):
metric = self.file.create_entity(
"IfcMetric",
**{
"Name": "Unnamed",
"ConstraintGrade": "NOTDEFINED",
"Benchmark": "EQUALTO",
}
)
if self.settings["objective"]:
benchmark_values = list(self.settings["objective"].BenchmarkValues or [])
benchmark_values.append(metric)
self.settings["objective"].BenchmarkValues = benchmark_values
return metric
metric = file.create_entity(
"IfcMetric",
**{
"Name": "Unnamed",
"ConstraintGrade": "NOTDEFINED",
"Benchmark": "EQUALTO",
},
)
if settings["objective"]:
benchmark_values = list(settings["objective"].BenchmarkValues or [])
benchmark_values.append(metric)
settings["objective"].BenchmarkValues = benchmark_values
return metric
@@ -18,28 +18,28 @@
import ifcopenshell
class Usecase:
def __init__(self, file, metric=None, reference_path=None):
"""
Adds a chain of references to a metric. The reference path is a string of the form "attribute.attribute.attribute"
Used to reference a value of an attribute of an instance through a metric objective entity.
"""
self.file = file
self.settings = {"metric": metric, "reference_path": reference_path}
def execute(self):
if self.settings["reference_path"]:
attributes = self.settings["reference_path"].split(".")
references_created = []
for i in range(len(attributes)):
if i == 0:
reference = self.file.create_entity("IfcReference")
reference.AttributeIdentifier = attributes[i]
self.settings["metric"].ReferencePath = reference
references_created.append(reference)
else:
reference = self.file.create_entity("IfcReference")
reference.AttributeIdentifier = attributes[i]
references_created[i-1].InnerReference = reference
references_created.append(reference)
return references_created
def add_metric_reference(
file: ifcopenshell.file, metric: ifcopenshell.entity_instance, reference_path: str
) -> list[ifcopenshell.entity_instance]:
"""
Adds a chain of references to a metric. The reference path is a string of the form "attribute.attribute.attribute"
Used to reference a value of an attribute of an instance through a metric objective entity.
"""
settings = {"metric": metric, "reference_path": reference_path}
references_created = []
if settings["reference_path"]:
attributes = settings["reference_path"].split(".")
for i in range(len(attributes)):
if i == 0:
reference = file.create_entity("IfcReference")
reference.AttributeIdentifier = attributes[i]
settings["metric"].ReferencePath = reference
references_created.append(reference)
else:
reference = file.create_entity("IfcReference")
reference.AttributeIdentifier = attributes[i]
references_created[i - 1].InnerReference = reference
references_created.append(reference)
return references_created
@@ -19,34 +19,31 @@
import ifcopenshell
class Usecase:
def __init__(self, file):
"""Add a new objective constraint
def add_objective(file: ifcopenshell.file) -> ifcopenshell.entity_instance:
"""Add a new objective constraint
Parametric constraints may be defined by the user. The constraint is defined
by first creating an objective describing the purpose of the constraint and
whether it is a hard or soft constraint. Later on, metrics may be added to
check whether the constraint has been met by connecting it to properties and
quantities. See ifcopenshell.api.constraint.add_metric for more information.
Parametric constraints may be defined by the user. The constraint is defined
by first creating an objective describing the purpose of the constraint and
whether it is a hard or soft constraint. Later on, metrics may be added to
check whether the constraint has been met by connecting it to properties and
quantities. See ifcopenshell.api.constraint.add_metric for more information.
:return: The newly created IfcObjective entity
:rtype: ifcopenshell.entity_instance.entity_instance
:return: The newly created IfcObjective entity
:rtype: ifcopenshell.entity_instance
Example:
Example:
.. code:: python
.. code:: python
# Create a new objective for code compliance requirements
objective = ifcopenshell.api.run("constraint.add_objective", model)
objective.ConstraintGrade = "ADVISORY"
objective.ObjectiveQualifier = "CODECOMPLIANCE"
# Note: the objective right now is purely qualitative and for
# information purposes. You may wish to add quantiative metrics.
"""
self.file = file
self.settings = {}
# Create a new objective for code compliance requirements
objective = ifcopenshell.api.run("constraint.add_objective", model)
objective.ConstraintGrade = "ADVISORY"
objective.ObjectiveQualifier = "CODECOMPLIANCE"
# Note: the objective right now is purely qualitative and for
# information purposes. You may wish to add quantiative metrics.
"""
settings = {}
def execute(self):
return self.file.create_entity(
"IfcObjective", **{"Name": "Unnamed", "ConstraintGrade": "NOTDEFINED", "ObjectiveQualifier": "NOTDEFINED"}
)
return file.create_entity(
"IfcObjective", **{"Name": "Unnamed", "ConstraintGrade": "NOTDEFINED", "ObjectiveQualifier": "NOTDEFINED"}
)
@@ -18,42 +18,45 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
from typing import Union
def assign_constraint(
file: ifcopenshell.file,
products: list[ifcopenshell.entity_instance],
constraint: ifcopenshell.entity_instance,
) -> Union[ifcopenshell.entity_instance, None]:
"""Assigns a constraint to a list of products
This assigns a relationship between a product and a constraint, so that
when a product's properties and quantities do not match the requirements
of the constraint's metrics, results can be flagged.
It is assumed (but not explicit in the IFC documentation) that
constraints are inherited from the type. This way, it is not necessary
to create lots of constraint assignments.
:param products: The list of products the constraint applies to. This is anything
which can have properties or quantities.
:type products: list[ifcopenshell.entity_instance]
:param constraint: The IfcObjective constraint
:type constraint: ifcopenshell.entity_instance
:return: The new or updated IfcRelAssociatesConstraint relationship
or `None` if `products` was an empty list.
:rtype: ifcopenshell.entity_instance
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"products": products,
"constraint": constraint,
}
return usecase.execute()
class Usecase:
def __init__(
self,
file: ifcopenshell.file,
products: list[ifcopenshell.entity_instance],
constraint: ifcopenshell.entity_instance,
):
"""Assigns a constraint to a list of products
This assigns a relationship between a product and a constraint, so that
when a product's properties and quantities do not match the requirements
of the constraint's metrics, results can be flagged.
It is assumed (but not explicit in the IFC documentation) that
constraints are inherited from the type. This way, it is not necessary
to create lots of constraint assignments.
:param products: The list of products the constraint applies to. This is anything
which can have properties or quantities.
:type products: list[ifcopenshell.entity_instance.entity_instance]
:param constraint: The IfcObjective constraint
:type constraint: ifcopenshell.entity_instance.entity_instance
:return: The new or updated IfcRelAssociatesConstraint relationship
or `None` if `products` was an empty list.
:rtype: ifcopenshell.entity_instance.entity_instance
"""
self.file = file
self.settings = {
"products": products,
"constraint": constraint,
}
def execute(self) -> Union[ifcopenshell.entity_instance, None]:
def execute(self):
products = set(self.settings["products"])
if not products:
return
@@ -15,35 +15,34 @@
#
# 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
from typing import Any
class Usecase:
def __init__(self, file, metric=None, attributes=None):
"""Edit the attributes of a metric
def edit_metric(file: ifcopenshell.file, metric: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
"""Edit the attributes of a metric
For more information about the attributes and data types of an
IfcMetric, consult the IFC documentation.
For more information about the attributes and data types of an
IfcMetric, consult the IFC documentation.
:param metric: The IfcMetric you want to edit.
:type metric: ifcopenshell.entity_instance.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:return: None
:rtype: None
:param metric: The IfcMetric you want to edit.
:type metric: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
objective = ifcopenshell.api.run("constraint.add_objective", model)
metric = ifcopenshell.api.run("constraint.add_metric", model,
objective=objective)
ifcopenshell.api.run("constraint.edit_metric", model,
metric=metric, attributes={"ConstraintGrade": "HARD"})
"""
self.file = file
self.settings = {"metric": metric, "attributes": attributes or {}}
objective = ifcopenshell.api.run("constraint.add_objective", model)
metric = ifcopenshell.api.run("constraint.add_metric", model,
objective=objective)
ifcopenshell.api.run("constraint.edit_metric", model,
metric=metric, attributes={"ConstraintGrade": "HARD"})
"""
settings = {"metric": metric, "attributes": attributes or {}}
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["metric"], name, value)
for name, value in settings["attributes"].items():
setattr(settings["metric"], name, value)
@@ -15,33 +15,34 @@
#
# 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
from typing import Any
class Usecase:
def __init__(self, file, objective=None, attributes=None):
"""Edit the attributes of a objective
def edit_objective(
file: ifcopenshell.file, objective: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edit the attributes of a objective
For more information about the attributes and data types of an
IfcObjective, consult the IFC documentation.
For more information about the attributes and data types of an
IfcObjective, consult the IFC documentation.
:param objective: The IfcObjective you want to edit.
:type objective: ifcopenshell.entity_instance.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:return: None
:rtype: None
:param objective: The IfcObjective you want to edit.
:type objective: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
objective = ifcopenshell.api.run("constraint.add_objective", model)
ifcopenshell.api.run("constraint.edit_objective", model,
objective=objective, attributes={"ConstraintGrade": "HARD"})
"""
self.file = file
self.settings = {"objective": objective, "attributes": attributes or {}}
objective = ifcopenshell.api.run("constraint.add_objective", model)
ifcopenshell.api.run("constraint.edit_objective", model,
objective=objective, attributes={"ConstraintGrade": "HARD"})
"""
settings = {"objective": objective, "attributes": attributes or {}}
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["objective"], name, value)
for name, value in settings["attributes"].items():
setattr(settings["objective"], name, value)
@@ -20,36 +20,33 @@ import ifcopenshell
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, constraint=None):
"""Remove a constraint (typically an objective)
def remove_constraint(file: ifcopenshell.file, constraint: ifcopenshell.entity_instance) -> None:
"""Remove a constraint (typically an objective)
Removes a constraint definition and all of its associations to any
products. Typically this would be an IfcObjective, although technically
you can associate IfcMetrics ith products too, though the meaning may be
unclear.
Removes a constraint definition and all of its associations to any
products. Typically this would be an IfcObjective, although technically
you can associate IfcMetrics ith products too, though the meaning may be
unclear.
:param constraint: The IfcObjective you want to remove.
:type constraint: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param constraint: The IfcObjective you want to remove.
:type constraint: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
objective = ifcopenshell.api.run("constraint.add_objective", model)
ifcopenshell.api.run("constraint.remove_constraint", model,
constraint=objective)
"""
self.file = file
self.settings = {"constraint": constraint}
objective = ifcopenshell.api.run("constraint.add_objective", model)
ifcopenshell.api.run("constraint.remove_constraint", model,
constraint=objective)
"""
settings = {"constraint": constraint}
def execute(self):
self.file.remove(self.settings["constraint"])
for rel in self.file.by_type("IfcRelAssociatesConstraint"):
if not rel.RelatingConstraint:
history = rel.OwnerHistory
self.file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
file.remove(settings["constraint"])
for rel in file.by_type("IfcRelAssociatesConstraint"):
if not rel.RelatingConstraint:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
@@ -15,33 +15,37 @@
#
# 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
def remove_metric(file: ifcopenshell.file, metric: ifcopenshell.entity_instance) -> None:
"""Remove a metric benchmark
Removes a metric benchmark and all of its associations to any products
and objectives.
:param metric: The IfcMetric you want to remove.
:type metric: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
.. code:: python
objective = ifcopenshell.api.run("constraint.add_objective", model)
metric = ifcopenshell.api.run("constraint.add_metric", model,
objective=objective)
ifcopenshell.api.run("constraint.remove_metric", model,
metric=metric)
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {"metric": metric}
return usecase.execute()
class Usecase:
def __init__(self, file, metric=None):
"""Remove a metric benchmark
Removes a metric benchmark and all of its associations to any products
and objectives.
:param metric: The IfcMetric you want to remove.
:type metric: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
Example:
.. code:: python
objective = ifcopenshell.api.run("constraint.add_objective", model)
metric = ifcopenshell.api.run("constraint.add_metric", model,
objective=objective)
ifcopenshell.api.run("constraint.remove_metric", model,
metric=metric)
"""
self.file = file
self.settings = {"metric": metric}
def execute(self):
if self.settings["metric"].ReferencePath:
reference = self.settings["metric"].ReferencePath
@@ -21,31 +21,33 @@ import ifcopenshell.api
import ifcopenshell.util.element
def unassign_constraint(
file: ifcopenshell.file,
products: list[ifcopenshell.entity_instance],
constraint: ifcopenshell.entity_instance,
) -> None:
"""Unassigns a constraint from a list of products
The constraint will not be deleted and is available to be assigned to
other products.
:param products: The list of products the constraint applies to.
:type products: list[ifcopenshell.entity_instance]
:param constraint: The IfcObjective constraint
:type constraint: ifcopenshell.entity_instance
:return: None
:rtype: None
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"products": products,
"constraint": constraint,
}
return usecase.execute()
class Usecase:
def __init__(
self,
file: ifcopenshell.file,
products: list[ifcopenshell.entity_instance],
constraint: ifcopenshell.entity_instance,
):
"""Unassigns a constraint from a list of products
The constraint will not be deleted and is available to be assigned to
other products.
:param products: The list of products the constraint applies to.
:type products: list[ifcopenshell.entity_instance.entity_instance]
:param constraint: The IfcObjective constraint
:type constraint: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
"""
self.file = file
self.settings = {
"products": products,
"constraint": constraint,
}
def execute(self):
products = set(self.settings["products"])
if not products:
@@ -15,3 +15,25 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Contexts allow you to classify when geometry should be used in different
purposes
For example, a door may have many geometries assigned to it: a 3D body
geometry, a clearance zone for disabled access and egress, and a 2D top down
plan view representation annotating swing extents. Each geometry is assigned to
a context to distinguish its purpose and level of detail.
"""
from .. import wrap_usecases
from .add_context import add_context
from .edit_context import edit_context
from .remove_context import remove_context
wrap_usecases(__path__, __name__)
__all__ = [
"add_context",
"edit_context",
"remove_context",
]
@@ -16,169 +16,181 @@
# 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
from typing import Optional, Literal
def add_context(
file: ifcopenshell.file,
context_type: Optional[Literal["Model", "Plan"]] = None,
context_identifier: Optional[str] = None,
target_view: Optional[str] = None,
parent: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance:
"""Adds a new geometric representation context
In IFC, physical objects may have zero, one, or multiple geometric
representations associated with it. For example, a building storey might
not have any geometry, but simply be a coordinate in space.
Alternatively, a wall might have a 3D body representation in the form of
a cuboid. As a final example, a door might also have a 3D body
representation of a 3D door panel and door frame, but may additionally
have a 2D door plan view representation of the door swing, and even a 2D
elevation view of the door, a 3D box representing the disabled clearance
zone of the door, a 2D profile representing the profile of the door to
cut out in a wall, and so on. In this situation, a door will have
multiple geometric representations.
To distinguish between the different purposes of multiple geometric
representations, each geometric representation must belong to a
geometric representation "context". There are typically always 2
contexts, one for 3D representations and one for 2D representations.
These 2 contexts then have subcontexts for things like the 3D body
representation, clearance representations, annotation representations,
and so on. Each representation of a physical IFC product (e.g. a door)
must be assigned to one of these subcontexts. Therefore setting up
appropriate contexts is critical prior to authoring any IFC model which
contains geometry.
There are two steps to setting up appropriate subcontexts. First, a 2D
and/or 3D context must be added. These must be always called the "Model"
context for 3D and the "Plan" context for 2D (even if the 2D geometry is
not a plan view). Then, one or more subcontexts are added using either
the "Model" or "Plan" as their parent. These subcontexts are further
distinguished using an "identifier" and "target view". The "identifier"
describes the purpose of the representation, and the "target view"
describes the typical diagrammatic presentation that context's geometry
should be viewed in. The most common identifiers you might use are:
- Body: for the actual shape of the object
- Box: the bounding box of the object (useful for shape analytics)
- Axis: the parametric line determining the shape of the object
- Profile: the elevation silhouette of the object, useful for cutting
out holes for the object to fit into host elements
- Footprint: the plan view silhouette of the object, useful for certain
quantity take-off rules
- Clearance: the clearance zone of the object
- Annotation: symbolic annotations typically used in diagrams or
drawings
The most common "target views" you might use are:
- MODEL_VIEW: for 3D geometry you might see in a BIM viewer
- PLAN_VIEW: for 2D geometry you might see in a plan representation
- ELEVATION_VIEW: for 2D geometry you might see in an elevation representation
- SECTION_VIEW: for 2D geometry you might see in a section representation
- GRAPH_VIEW: for 2D or 3D line or frame or path connectivity diagrams
you might use for structural frame analysis, axis-based parametric
modeling
- SKETCH_VIEW: for viewing abstract high-level representations such as
in bubble diagrams of spatial topology
This may sound like a lot, but after a few typical contexts are set up
at the beginning, it becomes easy to navigate and isolate geometry for
different purposes. There is also the concept of a target scale, which
represents the zoom level detail of geometry, but this is not currently
supported by this API. Setting up all these contexts are also optional,
and you may only use a single Model context and Body subcontext for
simple models, but this simplification sacrifices the ability of more
parametric or analytical usecases.
:param context_type: The type of the context, must be one of "Model" or
"Plan" only.
:type context_type: str, optional
:param context_identifier: The identifier of the context, chosen from
one of the common identifiers above or consult the IFC documentation
(under the IfcShapeRepresentation page) for more details. Optional
for contexts, but mandatory for subcontexts.
:type context_identifier: str, optional
:param target_view: the target view of the context, chosen from one of
the common target views above or consult the IFC documentation
(under the IfcShapeRepresentation page) for more details. Optional
for contexts, but mandatory for subcontexts.
:type target_view: str, optional
:param parent: the parent context. Must be left as None (the default)
for contexts, and only set for subcontexts. Note that there are only
contexts and subcontexts, a subcontext cannot have any children.
:type parent: ifcopenshell.entity_instance, optional
:return: the newly created IfcGeometricRepresentationContext or
IfcGeometricRepresentationSubContext entity
:rtype: ifcopenshell.entity_instance
Example:
.. code:: python
# If we plan to store 3D geometry in our IFC model, we have to setup
# a "Model" context.
model3d = ifcopenshell.api.run("context.add_context", model, context_type="Model")
# And/Or, if we plan to store 2D geometry, we need a "Plan" context
plan = ifcopenshell.api.run("context.add_context", model, context_type="Plan")
# Now we setup the subcontexts with each of the geometric "purposes"
# we plan to store in our model. "Body" is by far the most important
# and common context, as most IFC models are assumed to be viewable
# in 3D.
body = ifcopenshell.api.run("context.add_context", model,
context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model3d)
# The 3D Axis subcontext is important if any "axis-based" parametric
# geometry is going to be created. For example, a beam, or column
# may be drawn using a single 3D axis line, and for this we need an
# Axis subcontext.
ifcopenshell.api.run("context.add_context", model,
context_type="Model", context_identifier="Axis", target_view="GRAPH_VIEW", parent=model3d)
# The 3D Box subcontext is useful for clash detection or shape
# analysis, or even lazy-loading of large models.
ifcopenshell.api.run("context.add_context", model,
context_type="Model", context_identifier="Box", target_view="MODEL_VIEW", parent=model3d)
# It's also important to have a 2D Axis subcontext for things like
# walls and claddings which can be drawn using a 2D axis line.
ifcopenshell.api.run("context.add_context", model,
context_type="Plan", context_identifier="Axis", target_view="GRAPH_VIEW", parent=plan)
# A 2D annotation subcontext for plan views are important for door
# swings, window cuts, and symbols for equipment like GPOs, fire
# extinguishers, and so on.
ifcopenshell.api.run("context.add_context", model,
context_type="Plan", context_identifier="Annotation", target_view="PLAN_VIEW", parent=plan)
# You may also create 2D annotation subcontexts for sections and
# elevation views.
ifcopenshell.api.run("context.add_context", model,
context_type="Plan", context_identifier="Annotation", target_view="SECTION_VIEW", parent=plan)
ifcopenshell.api.run("context.add_context", model,
context_type="Plan", context_identifier="Annotation", target_view="ELEVATION_VIEW", parent=plan)
# Let's create a new wall. The wall does not have any geometry yet.
wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall")
# Let's use the "3D Body" representation we created earlier to add a
# new wall-like body geometry, 5 meters long, 3 meters high, and
# 200mm thick
representation = ifcopenshell.api.run("geometry.add_wall_representation", model,
context=body, length=5, height=3, thickness=0.2)
# Assign our new body geometry back to our wall
ifcopenshell.api.run("geometry.assign_representation", model,
product=wall, representation=representation)
# Place our wall at the origin
ifcopenshell.api.run("geometry.edit_object_placement", model, product=wall)
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"context_type": context_type,
"parent": parent,
"context_identifier": context_identifier,
"target_view": target_view,
}
return usecase.execute()
class Usecase:
def __init__(self, file, context_type=None, context_identifier=None, target_view=None, parent=None):
"""Adds a new geometric representation context
In IFC, physical objects may have zero, one, or multiple geometric
representations associated with it. For example, a building storey might
not have any geometry, but simply be a coordinate in space.
Alternatively, a wall might have a 3D body representation in the form of
a cuboid. As a final example, a door might also have a 3D body
representation of a 3D door panel and door frame, but may additionally
have a 2D door plan view representation of the door swing, and even a 2D
elevation view of the door, a 3D box representing the disabled clearance
zone of the door, a 2D profile representing the profile of the door to
cut out in a wall, and so on. In this situation, a door will have
multiple geometric representations.
To distinguish between the different purposes of multiple geometric
representations, each geometric representation must belong to a
geometric representation "context". There are typically always 2
contexts, one for 3D representations and one for 2D representations.
These 2 contexts then have subcontexts for things like the 3D body
representation, clearance representations, annotation representations,
and so on. Each representation of a physical IFC product (e.g. a door)
must be assigned to one of these subcontexts. Therefore setting up
appropriate contexts is critical prior to authoring any IFC model which
contains geometry.
There are two steps to setting up appropriate subcontexts. First, a 2D
and/or 3D context must be added. These must be always called the "Model"
context for 3D and the "Plan" context for 2D (even if the 2D geometry is
not a plan view). Then, one or more subcontexts are added using either
the "Model" or "Plan" as their parent. These subcontexts are further
distinguished using an "identifier" and "target view". The "identifier"
describes the purpose of the representation, and the "target view"
describes the typical diagrammatic presentation that context's geometry
should be viewed in. The most common identifiers you might use are:
- Body: for the actual shape of the object
- Box: the bounding box of the object (useful for shape analytics)
- Axis: the parametric line determining the shape of the object
- Profile: the elevation silhouette of the object, useful for cutting
out holes for the object to fit into host elements
- Footprint: the plan view silhouette of the object, useful for certain
quantity take-off rules
- Clearance: the clearance zone of the object
- Annotation: symbolic annotations typically used in diagrams or
drawings
The most common "target views" you might use are:
- MODEL_VIEW: for 3D geometry you might see in a BIM viewer
- PLAN_VIEW: for 2D geometry you might see in a plan representation
- ELEVATION_VIEW: for 2D geometry you might see in an elevation representation
- SECTION_VIEW: for 2D geometry you might see in a section representation
- GRAPH_VIEW: for 2D or 3D line or frame or path connectivity diagrams
you might use for structural frame analysis, axis-based parametric
modeling
- SKETCH_VIEW: for viewing abstract high-level representations such as
in bubble diagrams of spatial topology
This may sound like a lot, but after a few typical contexts are set up
at the beginning, it becomes easy to navigate and isolate geometry for
different purposes. There is also the concept of a target scale, which
represents the zoom level detail of geometry, but this is not currently
supported by this API. Setting up all these contexts are also optional,
and you may only use a single Model context and Body subcontext for
simple models, but this simplification sacrifices the ability of more
parametric or analytical usecases.
:param context_type: The type of the context, must be one of "Model" or
"Plan" only.
:type context_type: str
:param context_identifier: The identifier of the context, chosen from
one of the common identifiers above or consult the IFC documentation
(under the IfcShapeRepresentation page) for more details. Optional
for contexts, but mandatory for subcontexts.
:type context_identifier: str, optional
:param target_view: the target view of the context, chosen from one of
the common target views above or consult the IFC documentation
(under the IfcShapeRepresentation page) for more details. Optional
for contexts, but mandatory for subcontexts.
:type target_view: str, optional
:param parent: the parent context. Must be left as None (the default)
for contexts, and only set for subcontexts. Note that there are only
contexts and subcontexts, a subcontext cannot have any children.
:type parent: ifcopenshell.entity_instance.entity_instance, optional
:return: the newly created IfcGeometricRepresentationContext or
IfcGeometricRepresentationSubContext entity
:rtype: ifcopenshell.entity_instance.entity_instance, optional
Example:
.. code:: python
# If we plan to store 3D geometry in our IFC model, we have to setup
# a "Model" context.
model3d = ifcopenshell.api.run("context.add_context", model, context_type="Model")
# And/Or, if we plan to store 2D geometry, we need a "Plan" context
plan = ifcopenshell.api.run("context.add_context", model, context_type="Plan")
# Now we setup the subcontexts with each of the geometric "purposes"
# we plan to store in our model. "Body" is by far the most important
# and common context, as most IFC models are assumed to be viewable
# in 3D.
body = ifcopenshell.api.run("context.add_context", model,
context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model3d)
# The 3D Axis subcontext is important if any "axis-based" parametric
# geometry is going to be created. For example, a beam, or column
# may be drawn using a single 3D axis line, and for this we need an
# Axis subcontext.
ifcopenshell.api.run("context.add_context", model,
context_type="Model", context_identifier="Axis", target_view="GRAPH_VIEW", parent=model3d)
# The 3D Box subcontext is useful for clash detection or shape
# analysis, or even lazy-loading of large models.
ifcopenshell.api.run("context.add_context", model,
context_type="Model", context_identifier="Box", target_view="MODEL_VIEW", parent=model3d)
# It's also important to have a 2D Axis subcontext for things like
# walls and claddings which can be drawn using a 2D axis line.
ifcopenshell.api.run("context.add_context", model,
context_type="Plan", context_identifier="Axis", target_view="GRAPH_VIEW", parent=plan)
# A 2D annotation subcontext for plan views are important for door
# swings, window cuts, and symbols for equipment like GPOs, fire
# extinguishers, and so on.
ifcopenshell.api.run("context.add_context", model,
context_type="Plan", context_identifier="Annotation", target_view="PLAN_VIEW", parent=plan)
# You may also create 2D annotation subcontexts for sections and
# elevation views.
ifcopenshell.api.run("context.add_context", model,
context_type="Plan", context_identifier="Annotation", target_view="SECTION_VIEW", parent=plan)
ifcopenshell.api.run("context.add_context", model,
context_type="Plan", context_identifier="Annotation", target_view="ELEVATION_VIEW", parent=plan)
# Let's create a new wall. The wall does not have any geometry yet.
wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall")
# Let's use the "3D Body" representation we created earlier to add a
# new wall-like body geometry, 5 meters long, 3 meters high, and
# 200mm thick
representation = ifcopenshell.api.run("geometry.add_wall_representation", model,
context=body, length=5, height=3, thickness=0.2)
# Assign our new body geometry back to our wall
ifcopenshell.api.run("geometry.assign_representation", model,
product=wall, representation=representation)
# Place our wall at the origin
ifcopenshell.api.run("geometry.edit_object_placement", model, product=wall)
"""
self.file = file
self.settings = {
"context_type": context_type,
"parent": parent,
"context_identifier": context_identifier,
"target_view": target_view,
}
def execute(self):
if not self.settings["parent"]:
if self.settings["context_type"] == "Plan":
@@ -16,38 +16,38 @@
# 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
from typing import Any
class Usecase:
def __init__(self, file, context, attributes):
"""Edits the attributes of an IfcGeometricRepresentationContext
For more information about the attributes and data types of an
IfcGeometricRepresentationContext, consult the IFC documentation.
def edit_context(file: ifcopenshell.file, context: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
"""Edits the attributes of an IfcGeometricRepresentationContext
:param context: The IfcGeometricRepresentationContext entity you want to edit
:type context: ifcopenshell.entity_instance.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:return: None
:rtype: None
For more information about the attributes and data types of an
IfcGeometricRepresentationContext, consult the IFC documentation.
Example:
:param context: The IfcGeometricRepresentationContext entity you want to edit
:type context: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict
:return: None
:rtype: None
.. code:: python
Example:
model = ifcopenshell.api.run("context.add_context", model, context_type="Model")
# Revit had a bug where they incorrectly called the body representation a "Facetation"
body = ifcopenshell.api.run("context.add_context", model,
context_type="Model", context_identifier="Facetation", target_view="MODEL_VIEW", parent=model
)
.. code:: python
# Let's fix it!
ifcopenshell.api.run("context.edit_context", model,
context=body, attributes={"ContextIdentifier": "Body"})
"""
self.file = file
self.settings = {"context": context, "attributes": attributes or {}}
model = ifcopenshell.api.run("context.add_context", model, context_type="Model")
# Revit had a bug where they incorrectly called the body representation a "Facetation"
body = ifcopenshell.api.run("context.add_context", model,
context_type="Model", context_identifier="Facetation", target_view="MODEL_VIEW", parent=model
)
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["context"], name, value)
# Let's fix it!
ifcopenshell.api.run("context.edit_context", model,
context=body, attributes={"ContextIdentifier": "Body"})
"""
settings = {"context": context, "attributes": attributes}
for name, value in settings["attributes"].items():
setattr(settings["context"], name, value)
@@ -17,51 +17,50 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, context=None):
"""Removes an IfcGeometricRepresentationContext
def remove_context(file: ifcopenshell.file, context: ifcopenshell.entity_instance) -> None:
"""Removes an IfcGeometricRepresentationContext
Any representation geometry that is assigned to the context is also
removed. If a context is removed, then any subcontexts are also removed.
Any representation geometry that is assigned to the context is also
removed. If a context is removed, then any subcontexts are also removed.
:param context: The IfcGeometricRepresentationContext entity to remove
:type context: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param context: The IfcGeometricRepresentationContext entity to remove
:type context: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
model = ifcopenshell.api.run("context.add_context", model, context_type="Model")
# Revit had a bug where they incorrectly called the body representation a "Facetation"
body = ifcopenshell.api.run("context.add_context", model,
context_type="Model", context_identifier="Facetation", target_view="MODEL_VIEW", parent=model
)
model = ifcopenshell.api.run("context.add_context", model, context_type="Model")
# Revit had a bug where they incorrectly called the body representation a "Facetation"
body = ifcopenshell.api.run("context.add_context", model,
context_type="Model", context_identifier="Facetation", target_view="MODEL_VIEW", parent=model
)
# Let's just get rid of it completely
ifcopenshell.api.run("context.remove_context", model, context=body)
"""
self.file = file
self.settings = {"context": context}
# Let's just get rid of it completely
ifcopenshell.api.run("context.remove_context", model, context=body)
"""
settings = {"context": context}
def execute(self):
for subcontext in self.settings["context"].HasSubContexts:
ifcopenshell.api.run("context.remove_context", self.file, context=subcontext)
for subcontext in settings["context"].HasSubContexts:
ifcopenshell.api.run("context.remove_context", file, context=subcontext)
if getattr(self.settings["context"], "ParentContext", None):
new = self.settings["context"].ParentContext
for inverse in self.file.get_inverse(self.settings["context"]):
if inverse.is_a("IfcCoordinateOperation"):
inverse.SourceCRS = inverse.TargetCRS
ifcopenshell.util.element.remove_deep(self.file, inverse)
else:
ifcopenshell.util.element.replace_attribute(inverse, self.settings["context"], new)
self.file.remove(self.settings["context"])
else:
representations_in_context = self.settings["context"].RepresentationsInContext
self.file.remove(self.settings["context"])
for element in representations_in_context:
ifcopenshell.api.run("geometry.remove_representation", self.file, representation=element)
if getattr(settings["context"], "ParentContext", None):
new = settings["context"].ParentContext
for inverse in file.get_inverse(settings["context"]):
if inverse.is_a("IfcCoordinateOperation"):
inverse.SourceCRS = inverse.TargetCRS
ifcopenshell.util.element.remove_deep(file, inverse)
else:
ifcopenshell.util.element.replace_attribute(inverse, settings["context"], new)
file.remove(settings["context"])
else:
representations_in_context = settings["context"].RepresentationsInContext
file.remove(settings["context"])
for element in representations_in_context:
ifcopenshell.api.run("geometry.remove_representation", file, representation=element)
@@ -15,3 +15,20 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Processes and costs may be controlled by other entities which indicate
constraints that determine how they can change
This is an advanced feature mostly used in 4D/5D
"""
from .. import wrap_usecases
from .assign_control import assign_control
from .unassign_control import unassign_control
wrap_usecases(__path__, __name__)
__all__ = [
"assign_control",
"unassign_control",
]
@@ -18,89 +18,89 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
from typing import Union
class Usecase:
def __init__(self, file, relating_control=None, related_object=None):
"""Assigns a planning control or constraint to an object
def assign_control(
file: ifcopenshell.file,
relating_control: ifcopenshell.entity_instance,
related_object: ifcopenshell.entity_instance,
) -> Union[ifcopenshell.entity_instance, None]:
"""Assigns a planning control or constraint to an object
IFC can describe concepts that control other objects. For example, a
planning calendar controls the availability of working days for
construction planning. As another example, a cost item might constrain
or limit the ability to procure and build a product.
IFC can describe concepts that control other objects. For example, a
planning calendar controls the availability of working days for
construction planning. As another example, a cost item might constrain
or limit the ability to procure and build a product.
This usecase lets you assign controls following the rules of the IFC
specification. This is an advanced topic and assumes knowledge of the
IFC concepts to determine what is allowed to control what. In the
future, this API will likely be deprecated in favour of multiple usecase
specific APIs.
This usecase lets you assign controls following the rules of the IFC
specification. This is an advanced topic and assumes knowledge of the
IFC concepts to determine what is allowed to control what. In the
future, this API will likely be deprecated in favour of multiple usecase
specific APIs.
:param relating_control: The IfcControl entity that is creating the
control or constraint
:type relating_control: ifcopenshell.entity_instance.entity_instance
:param related_object: The IfcObjectDefinition that is being controlled
:type related_object: ifcopenshell.entity_instance.entity_instance
:return: The newly created IfcRelAssignsToControl. If relationship already
existed before and wasn't changed then returns None.
:rtype: ifcopenshell.entity_instance.entity_instance, None
:param relating_control: The IfcControl entity that is creating the
control or constraint
:type relating_control: ifcopenshell.entity_instance
:param related_object: The IfcObjectDefinition that is being controlled
:type related_object: ifcopenshell.entity_instance
:return: The newly created IfcRelAssignsToControl. If relationship already
existed before and wasn't changed then returns None.
:rtype: ifcopenshell.entity_instance, None
Example:
Example:
.. code:: python
.. code:: python
# One common usecase is to assign a calendar to a task
calendar = ifcopenshell.api.run("sequence.add_work_calendar", model)
schedule = ifcopenshell.api.run("sequence.add_work_schedule", model)
task = ifcopenshell.api.run("sequence.add_task", model,
work_schedule=schedule)
# One common usecase is to assign a calendar to a task
calendar = ifcopenshell.api.run("sequence.add_work_calendar", model)
schedule = ifcopenshell.api.run("sequence.add_work_schedule", model)
task = ifcopenshell.api.run("sequence.add_task", model,
work_schedule=schedule)
# All subtasks will inherit this calendar, so assigning a single
# calendar to the root task effectively defines a "default" calendar
ifcopenshell.api.run("control.assign_control", model,
relating_control=calendar, related_object=task)
# All subtasks will inherit this calendar, so assigning a single
# calendar to the root task effectively defines a "default" calendar
ifcopenshell.api.run("control.assign_control", model,
relating_control=calendar, related_object=task)
# Another common example might be relating a cost item and a product
wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall")
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
cost_item = ifcopenshell.api.run("cost.add_cost_item", model,
cost_schedule=schedule)
ifcopenshell.api.run("control.assign_control", model,
relating_control=cost_item, related_object=wall)
"""
self.file = file
self.settings = {
"relating_control": relating_control,
"related_object": related_object,
}
# Another common example might be relating a cost item and a product
wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall")
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
cost_item = ifcopenshell.api.run("cost.add_cost_item", model,
cost_schedule=schedule)
ifcopenshell.api.run("control.assign_control", model,
relating_control=cost_item, related_object=wall)
"""
settings = {
"relating_control": relating_control,
"related_object": related_object,
}
def execute(self):
if self.settings["related_object"].HasAssignments:
for assignment in self.settings["related_object"].HasAssignments:
if (
assignment.is_a("IfcRelAssignsToControl")
and assignment.RelatingControl == self.settings["relating_control"]
):
return
controls = None
if self.settings["relating_control"].Controls:
controls = self.settings["relating_control"].Controls[0]
if controls:
if self.settings["related_object"] in controls.RelatedObjects:
if settings["related_object"].HasAssignments:
for assignment in settings["related_object"].HasAssignments:
if assignment.is_a("IfcRelAssignsToControl") and assignment.RelatingControl == settings["relating_control"]:
return
related_objects = set(controls.RelatedObjects)
related_objects.add(self.settings["related_object"])
controls.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": controls})
else:
controls = self.file.create_entity(
"IfcRelAssignsToControl",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
"RelatedObjects": [self.settings["related_object"]],
"RelatingControl": self.settings["relating_control"],
},
)
return controls
controls = None
if settings["relating_control"].Controls:
controls = settings["relating_control"].Controls[0]
if controls:
if settings["related_object"] in controls.RelatedObjects:
return
related_objects = set(controls.RelatedObjects)
related_objects.add(settings["related_object"])
controls.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", file, **{"element": controls})
else:
controls = file.create_entity(
"IfcRelAssignsToControl",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", file),
"RelatedObjects": [settings["related_object"]],
"RelatingControl": settings["relating_control"],
},
)
return controls
@@ -19,56 +19,58 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
from typing import Union
class Usecase:
def __init__(self, file, relating_control=None, related_object=None):
"""Unassigns a planning control or constraint to an object
def unassign_control(
file: ifcopenshell.file,
relating_control: ifcopenshell.entity_instance,
related_object: ifcopenshell.entity_instance,
) -> Union[ifcopenshell.entity_instance, None]:
"""Unassigns a planning control or constraint to an object
:param relating_control: The IfcControl entity that is creating the
control or constraint
:type relating_control: ifcopenshell.entity_instance.entity_instance
:param related_object: The IfcObjectDefinition that is being controlled
:type related_object: ifcopenshell.entity_instance.entity_instance
:return: If the control still is related to other objects, the
IfcRelAssignsToControl is returned, otherwise None.
:rtype: ifcopenshell.entity_instance.entity_instance, None
:param relating_control: The IfcControl entity that is creating the
control or constraint
:type relating_control: ifcopenshell.entity_instance
:param related_object: The IfcObjectDefinition that is being controlled
:type related_object: ifcopenshell.entity_instance
:return: If the control still is related to other objects, the
IfcRelAssignsToControl is returned, otherwise None.
:rtype: ifcopenshell.entity_instance, None
Example:
Example:
.. code:: python
.. code:: python
# Let's relate a cost item and a product
wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall")
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
cost_item = ifcopenshell.api.run("cost.add_cost_item", model,
cost_schedule=schedule)
ifcopenshell.api.run("control.assign_control", model,
relating_control=cost_item, related_object=wall)
# Let's relate a cost item and a product
wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall")
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
cost_item = ifcopenshell.api.run("cost.add_cost_item", model,
cost_schedule=schedule)
ifcopenshell.api.run("control.assign_control", model,
relating_control=cost_item, related_object=wall)
# And now let's change our mind
ifcopenshell.api.run("control.unassign_control", model,
relating_control=cost_item, related_object=wall)
"""
# And now let's change our mind
ifcopenshell.api.run("control.unassign_control", model,
relating_control=cost_item, related_object=wall)
"""
self.file = file
self.settings = {
"relating_control": relating_control,
"related_object": related_object,
}
settings = {
"relating_control": relating_control,
"related_object": related_object,
}
def execute(self):
for rel in self.settings["related_object"].HasAssignments or []:
if not rel.is_a("IfcRelAssignsToControl") or rel.RelatingControl != self.settings["relating_control"]:
continue
if len(rel.RelatedObjects) == 1:
history = rel.OwnerHistory
self.file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
return
related_objects = list(rel.RelatedObjects)
related_objects.remove(self.settings["related_object"])
rel.RelatedObjects = related_objects
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
return rel
for rel in settings["related_object"].HasAssignments or []:
if not rel.is_a("IfcRelAssignsToControl") or rel.RelatingControl != settings["relating_control"]:
continue
if len(rel.RelatedObjects) == 1:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
return
related_objects = list(rel.RelatedObjects)
related_objects.remove(settings["related_object"])
rel.RelatedObjects = related_objects
ifcopenshell.api.run("owner.update_owner_history", file, **{"element": rel})
return rel
@@ -15,3 +15,56 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Manage cost schedules, cost items, cost estimation and parametric quantity
take-off
IFC supports storing cost schedules and detailed cost breakdown structures,
including formulas, subtotals, and parametric links to model element
quantities.
"""
from .. import wrap_usecases
from .add_cost_item import add_cost_item
from .add_cost_item_quantity import add_cost_item_quantity
from .add_cost_schedule import add_cost_schedule
from .add_cost_value import add_cost_value
from .assign_cost_item_quantity import assign_cost_item_quantity
from .assign_cost_value import assign_cost_value
from .calculate_cost_item_resource_value import calculate_cost_item_resource_value
from .copy_cost_item import copy_cost_item
from .copy_cost_item_values import copy_cost_item_values
from .edit_cost_item import edit_cost_item
from .edit_cost_item_quantity import edit_cost_item_quantity
from .edit_cost_schedule import edit_cost_schedule
from .edit_cost_value import edit_cost_value
from .edit_cost_value_formula import edit_cost_value_formula
from .remove_cost_item import remove_cost_item
from .remove_cost_item_quantity import remove_cost_item_quantity
from .remove_cost_schedule import remove_cost_schedule
from .remove_cost_value import remove_cost_value
from .unassign_cost_item_quantity import unassign_cost_item_quantity
wrap_usecases(__path__, __name__)
__all__ = [
"add_cost_item",
"add_cost_item_quantity",
"add_cost_schedule",
"add_cost_value",
"assign_cost_item_quantity",
"assign_cost_value",
"calculate_cost_item_resource_value",
"copy_cost_item",
"copy_cost_item_values",
"edit_cost_item",
"edit_cost_item_quantity",
"edit_cost_schedule",
"edit_cost_value",
"edit_cost_value_formula",
"remove_cost_item",
"remove_cost_item_quantity",
"remove_cost_schedule",
"remove_cost_value",
"unassign_cost_item_quantity",
]
@@ -17,57 +17,62 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api
import ifcopenshell.guid
from typing import Optional
class Usecase:
def __init__(self, file, cost_schedule=None, cost_item=None):
"""Add a new cost item
def add_cost_item(
file: ifcopenshell.file,
cost_schedule: Optional[ifcopenshell.entity_instance] = None,
cost_item: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance:
"""Add a new cost item
A cost item represents a single line item in a cost schedule. Cost items
may then be broken down into cost subitems.
A cost item represents a single line item in a cost schedule. Cost items
may then be broken down into cost subitems.
:param cost_schedule: If the cost item is to be added as a root or top
level cost item to a cost schedule, the IfcCostSchedule may be
specified. This is mutually exlclusive to the cost_item parameter.
:type cost_schedule: ifcopenshell.entity_instance.entity_instance
:param cost_item: If the cost item is to be added as a subitem to an
existing cost item, the parent IfcCostItem may be specified. This is
mutually exclusive to the cost_schedule parameter.
:type cost_item: ifcopenshell.entity_instance.entity_instance
:return: The newly created IfcCostItem
:rtype: ifcopenshell.entity_instance.entity_instance
Either `cost_schedule` or `cost_item` must be provided.
Example:
:param cost_schedule: If the cost item is to be added as a root or top
level cost item to a cost schedule, the IfcCostSchedule may be
specified. This is mutually exlclusive to the cost_item parameter.
:type cost_schedule: ifcopenshell.entity_instance, optional.
:param cost_item: If the cost item is to be added as a subitem to an
existing cost item, the parent IfcCostItem may be specified. This is
mutually exclusive to the cost_schedule parameter.
:type cost_item: ifcopenshell.entity_instance, optional
:return: The newly created IfcCostItem
:rtype: ifcopenshell.entity_instance
.. code:: python
Example:
# The very first cost item must be in a cost schedule
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
.. code:: python
# You may add cost items as top level item in the schedule
item1 = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
# The very first cost item must be in a cost schedule
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
# Alternatively you may add them as subitems
item2 = ifcopenshell.api.run("cost.add_cost_item", model, cost_item=item1)
"""
self.file = file
self.settings = {"cost_schedule": cost_schedule, "cost_item": cost_item}
# You may add cost items as top level item in the schedule
item1 = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
def execute(self):
cost_item = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcCostItem")
# Alternatively you may add them as subitems
item2 = ifcopenshell.api.run("cost.add_cost_item", model, cost_item=item1)
"""
settings = {"cost_schedule": cost_schedule, "cost_item": cost_item}
if self.settings["cost_schedule"]:
self.file.create_entity(
"IfcRelAssignsToControl",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
"RelatedObjects": [cost_item],
"RelatingControl": self.settings["cost_schedule"],
}
)
elif self.settings["cost_item"]:
ifcopenshell.api.run(
"nest.assign_object", self.file, related_objects=[cost_item], relating_object=self.settings["cost_item"]
)
return cost_item
cost_item = ifcopenshell.api.run("root.create_entity", file, ifc_class="IfcCostItem")
if settings["cost_schedule"]:
file.create_entity(
"IfcRelAssignsToControl",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", file),
"RelatedObjects": [cost_item],
"RelatingControl": settings["cost_schedule"],
},
)
elif settings["cost_item"]:
ifcopenshell.api.run(
"nest.assign_object", file, related_objects=[cost_item], relating_object=settings["cost_item"]
)
return cost_item
@@ -17,75 +17,79 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api
import ifcopenshell.util.unit
class Usecase:
def __init__(self, file, cost_item=None, ifc_class="IfcQuantityCount"):
"""Adds a new quantity associated with a cost item
def add_cost_item_quantity(
file: ifcopenshell.file,
cost_item: ifcopenshell.entity_instance,
ifc_class: ifcopenshell.util.unit.QUANTITY_CLASS = "IfcQuantityCount",
) -> ifcopenshell.entity_instance:
"""Adds a new quantity associated with a cost item
Cost items calculate their subtotal by multiplying the sum of the cost
item's "values" by the sum of the cost item's "quantities". The
quantities may be either parametrically linked to quantities measured on
physical product, or manually specified.
Cost items calculate their subtotal by multiplying the sum of the cost
item's "values" by the sum of the cost item's "quantities". The
quantities may be either parametrically linked to quantities measured on
physical product, or manually specified.
The quantity must be of a particular type, common examples are:
The quantity must be of a particular type, common examples are:
- IfcQuantityCount: to count the total occurrences of a product, useful
for things like doors, windows, and furniture
- IfcQuantityNumber: any other generic numeric quantity
- IfcQuantityLength
- IfcQuantityArea
- IfcQuantityVolume
- IfcQuantityWeight
- IfcQuantityTime
- IfcQuantityCount: to count the total occurrences of a product, useful
for things like doors, windows, and furniture
- IfcQuantityNumber: any other generic numeric quantity
- IfcQuantityLength
- IfcQuantityArea
- IfcQuantityVolume
- IfcQuantityWeight
- IfcQuantityTime
A cost item must not mix quantities of different types.
A cost item must not mix quantities of different types.
If an IfcQuantityCount is used, then this API will automatically count
all products that this cost item controls (see
ifcopenshell.api.controls.assign_control) and prefill that quantity.
If an IfcQuantityCount is used, then this API will automatically count
all products that this cost item controls (see
ifcopenshell.api.controls.assign_control) and prefill that quantity.
For all other quantity types, the quantity is left as zero and the user
must either manually specify the quantity or parametrically link it
using another API call.
For all other quantity types, the quantity is left as zero and the user
must either manually specify the quantity or parametrically link it
using another API call.
:param cost_item: The IfcCostItem to add the quantity to
:type cost_item: ifcopenshell.entity_instance.entity_instance
:param ifc_class: The type of quantity to add
:type ifc_class: str, optional
:return: The newly created quantity entity, chosen from the ifc_class
parameter
:rtype: ifcopenshell.entity_instance.entity_instance
:param cost_item: The IfcCostItem to add the quantity to
:type cost_item: ifcopenshell.entity_instance
:param ifc_class: The type of quantity to add
:type ifc_class: str, optional
:return: The newly created quantity entity, chosen from the ifc_class
parameter
:rtype: ifcopenshell.entity_instance
Example:
Example:
.. code:: python
.. code:: python
chair = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcFurniture")
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
ifcopenshell.api.run("control.assign_control", model,
relating_control=cost_item, related_object=chair)
chair = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcFurniture")
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
ifcopenshell.api.run("control.assign_control", model,
relating_control=item, related_object=chair)
# Let's assume we want to count the amount of chairs to calculate our cost item
# Because this is an IfcQuantityCount the count will be automatically set to "1" chair
ifcopenshell.api.run("cost.add_cost_item_quantity", model,
cost_item=item, ifc_class="IfcQuantityCount")
"""
self.file = file
self.settings = {"cost_item": cost_item, "ifc_class": ifc_class}
# Let's assume we want to count the amount of chairs to calculate our cost item
# Because this is an IfcQuantityCount the count will be automatically set to "1" chair
ifcopenshell.api.run("cost.add_cost_item_quantity", model,
cost_item=item, ifc_class="IfcQuantityCount")
"""
settings = {"cost_item": cost_item, "ifc_class": ifc_class}
def execute(self):
quantity = self.file.create_entity(self.settings["ifc_class"], Name="Unnamed")
quantity = file.create_entity(settings["ifc_class"], Name="Unnamed")
# 3 IfcPhysicalSimpleQuantity Value
# This is a bold assumption
# https://forums.buildingsmart.org/t/how-does-a-cost-item-know-that-it-is-counting-a-controlled-product/3564
if settings["ifc_class"] == "IfcQuantityCount":
count = 0
for rel in settings["cost_item"].Controls:
count += len(rel.RelatedObjects)
quantity[3] = count
else:
quantity[3] = 0.0
# This is a bold assumption
# https://forums.buildingsmart.org/t/how-does-a-cost-item-know-that-it-is-counting-a-controlled-product/3564
if self.settings["ifc_class"] == "IfcQuantityCount" and self.settings["cost_item"].Controls:
count = 0
for rel in self.settings["cost_item"].Controls:
count += len(rel.RelatedObjects)
quantity[3] = count
quantities = list(self.settings["cost_item"].CostQuantities or [])
quantities.append(quantity)
self.settings["cost_item"].CostQuantities = quantities
return quantity
quantities = list(settings["cost_item"].CostQuantities or [])
quantities.append(quantity)
settings["cost_item"].CostQuantities = quantities
return quantity
@@ -19,50 +19,62 @@
import ifcopenshell.api
import ifcopenshell.util.date
from datetime import datetime
from typing import Optional
class Usecase:
def __init__(self, file, name=None, predefined_type="NOTDEFINED"):
"""Add a new cost schedule
def add_cost_schedule(
file: ifcopenshell.file, name: Optional[str] = None, predefined_type: str = "NOTDEFINED"
) -> ifcopenshell.entity_instance:
"""Add a new cost schedule
A cost schedule is a group of cost items which typically represent a
cost plan or breakdown of the project. This may be used as an estimate,
bid, or actual cost.
A cost schedule is a group of cost items which typically represent a
cost plan or breakdown of the project. This may be used as an estimate,
bid, or actual cost.
Alternatively, a cost schedule may also represent a schedule of rates,
which include cost items which capture unit rates for different elements
or processes.
Alternatively, a cost schedule may also represent a schedule of rates,
which include cost items which capture unit rates for different elements
or processes.
As such, creating a cost schedule is necessary prior to creating and
managing any cost items.
As such, creating a cost schedule is necessary prior to creating and
managing any cost items.
:param name: The name of the cost schedule.
:type name: str, optional
:param predefined_type: The predefined type of the cost schedule, chosen
from a valid type in the IFC documentation for
IfcCostScheduleTypeEnum
:type predefined_type: str, optional
:return: The newly created IfcCostSchedule entity
:rtype: ifcopenshell.entity_instance.entity_instance
:param name: The name of the cost schedule.
:type name: str, optional
:param predefined_type: The predefined type of the cost schedule, chosen
from a valid type in the IFC documentation for
IfcCostScheduleTypeEnum
:type predefined_type: str, optional
:return: The newly created IfcCostSchedule entity
:rtype: ifcopenshell.entity_instance
Example:
Example:
.. code:: python
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
# Now that we have a cost schedule, we may add cost items to it
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
"""
self.file = file
self.settings = {"name": name, "predefined_type": predefined_type}
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
# Now that we have a cost schedule, we may add cost items to it
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
"""
settings = {"name": name, "predefined_type": predefined_type}
def execute(self):
cost_schedule = ifcopenshell.api.run(
"root.create_entity",
self.file,
ifc_class="IfcCostSchedule",
predefined_type=self.settings["predefined_type"],
name=self.settings["name"],
)
cost_schedule = ifcopenshell.api.run(
"root.create_entity",
file,
ifc_class="IfcCostSchedule",
predefined_type=settings["predefined_type"],
name=settings["name"],
)
if file.schema == "IFC2X3":
cost_schedule.UpdateDate = createIfcDateAndTime(file, datetime.now())
else:
cost_schedule.UpdateDate = ifcopenshell.util.date.datetime2ifc(datetime.now(), "IfcDateTime")
return cost_schedule
return cost_schedule
def createIfcDateAndTime(file: ifcopenshell.file, dt: datetime):
ifc_dt = file.create_entity("IfcDateAndTime")
ifc_dt.DateComponent = file.create_entity(
"IfcCalendarDate", **ifcopenshell.util.date.datetime2ifc(dt, "IfcCalendarDate")
)
ifc_dt.TimeComponent = file.create_entity("IfcLocalTime", **ifcopenshell.util.date.datetime2ifc(dt, "IfcLocalTime"))
return ifc_dt
@@ -15,97 +15,95 @@
#
# 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
class Usecase:
def __init__(self, file, parent=None):
"""Adds a new value or subvalue to a cost item
def add_cost_value(file: ifcopenshell.file, parent: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
"""Adds a new value or subvalue to a cost item
A cost item's subtotal can be specified in two ways.
A cost item's subtotal can be specified in two ways.
Option 1 is by simply manually specifying the subtotal value, which
represents the full cost of that cost item. This option occurs when a
cost item has no quantities associated with it.
Option 1 is by simply manually specifying the subtotal value, which
represents the full cost of that cost item. This option occurs when a
cost item has no quantities associated with it.
Option 2 is by specifying a unit cost value of the cost item, which is
then multiplied by the associated quantity of the cost item, to give us
the subtotal. This option occurs when a cost item has quantities
associated with it.
Option 2 is by specifying a unit cost value of the cost item, which is
then multiplied by the associated quantity of the cost item, to give us
the subtotal. This option occurs when a cost item has quantities
associated with it.
For either option 1 (full cost value) or option 2 (unit cost value), the
cost value may be specified as a single number, or as a sum of
subcomponents or formulas (e.g. multiplication by wastage factor, or
adding taxes or other adjustments).
For either option 1 (full cost value) or option 2 (unit cost value), the
cost value may be specified as a single number, or as a sum of
subcomponents or formulas (e.g. multiplication by wastage factor, or
adding taxes or other adjustments).
This function lets you add a single top level unit value to a cost item,
or alternatively price subcomponents by using the "parent" parameter.
This function lets you add a single top level unit value to a cost item,
or alternatively price subcomponents by using the "parent" parameter.
More advanced usage, which involves summing, subcategory-filtered costs,
and formulas are possible but not yet documented.
More advanced usage, which involves summing, subcategory-filtered costs,
and formulas are possible but not yet documented.
:param parent: A parent IfcCostItem, if specifying a price directly to a
cost item, or a top-level price component. Alternatively, this can
be set to a IfcCostValue, if specifying price subcomponents.
:type parent: ifcopenshell.entity_instance.entity_instance
:return: The newly created IfcCostValue
:rtype: ifcopenshell.entity_instance.entity_instance
:param parent: A parent IfcCostItem, if specifying a price directly to a
cost item, or a top-level price component. Alternatively, this can
be set to a IfcCostValue, if specifying price subcomponents.
:type parent: ifcopenshell.entity_instance
:return: The newly created IfcCostValue
:rtype: ifcopenshell.entity_instance
Example:
Example:
.. code:: python
.. code:: python
# We always need a schedule first prior to adding any cost items
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
# We always need a schedule first prior to adding any cost items
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
# Option 1: This cost item will have a full cost of 42.0
item1 = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item1)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 42.0})
# Option 1: This cost item will have a full cost of 42.0
item1 = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item1)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 42.0})
# Option 2: This cost item will have a unit cost of 5.0 per unit
# area, multiplied by the quantity of area specified explicitly as
# 3.0, giving us a subtotal cost of 15.0.
item2 = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item2)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 5.0})
quantity = ifcopenshell.api.run("cost.add_cost_item_quantity", model,
cost_item=item2, ifc_class="IfcQuantityVolume")
ifcopenshell.api.run("cost.edit_cost_item_quantity", model,
physical_quantity=quantity, "attributes": {"VolumeValue": 3.0})
# Option 2: This cost item will have a unit cost of 5.0 per unit
# area, multiplied by the quantity of area specified explicitly as
# 3.0, giving us a subtotal cost of 15.0.
item2 = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item2)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 5.0})
quantity = ifcopenshell.api.run("cost.add_cost_item_quantity", model,
cost_item=item2, ifc_class="IfcQuantityVolume")
ifcopenshell.api.run("cost.edit_cost_item_quantity", model,
physical_quantity=quantity, "attributes": {"VolumeValue": 3.0})
# A cost value may also be specified in terms of the sum of its
# subcomponents. In this case, it's broken down into 2 subvalues.
item1 = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item1)
subvalue1 = ifcopenshell.api.run("cost.add_cost_value", model, parent=value)
subvalue2 = ifcopenshell.api.run("cost.add_cost_value", model, parent=value)
# A cost value may also be specified in terms of the sum of its
# subcomponents. In this case, it's broken down into 2 subvalues.
item1 = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item1)
subvalue1 = ifcopenshell.api.run("cost.add_cost_value", model, parent=value)
subvalue2 = ifcopenshell.api.run("cost.add_cost_value", model, parent=value)
# This specifies that the value is the sum of all subitems
# regardless of their cost category. The first subvalue is 2.0 and
# the second is 3.0, giving a total value of 5.0.
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value, attributes={"Category": "*"})
ifcopenshell.api.run("cost.edit_cost_value", model,
cost_value=subvalue1, attributes={"AppliedValue": 2.0})
ifcopenshell.api.run("cost.edit_cost_value", model,
cost_value=subvalue2, attributes={"AppliedValue": 3.0})
"""
self.file = file
self.settings = {"parent": parent}
# This specifies that the value is the sum of all subitems
# regardless of their cost category. The first subvalue is 2.0 and
# the second is 3.0, giving a total value of 5.0.
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value, attributes={"Category": "*"})
ifcopenshell.api.run("cost.edit_cost_value", model,
cost_value=subvalue1, attributes={"AppliedValue": 2.0})
ifcopenshell.api.run("cost.edit_cost_value", model,
cost_value=subvalue2, attributes={"AppliedValue": 3.0})
"""
settings = {"parent": parent}
def execute(self):
value = self.file.create_entity("IfcCostValue")
if self.settings["parent"].is_a("IfcCostItem"):
values = list(self.settings["parent"].CostValues or [])
values.append(value)
self.settings["parent"].CostValues = values
elif self.settings["parent"].is_a("IfcConstructionResource"):
values = list(self.settings["parent"].BaseCosts or [])
values.append(value)
self.settings["parent"].BaseCosts = values
elif self.settings["parent"].is_a("IfcCostValue"):
values = list(self.settings["parent"].Components or [])
values.append(value)
self.settings["parent"].Components = values
return value
value = file.create_entity("IfcCostValue")
if settings["parent"].is_a("IfcCostItem"):
values = list(settings["parent"].CostValues or [])
values.append(value)
settings["parent"].CostValues = values
elif settings["parent"].is_a("IfcConstructionResource"):
values = list(settings["parent"].BaseCosts or [])
values.append(value)
settings["parent"].BaseCosts = values
elif settings["parent"].is_a("IfcCostValue"):
values = list(settings["parent"].Components or [])
values.append(value)
settings["parent"].Components = values
return value
@@ -17,84 +17,97 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api
from typing import Any
def assign_cost_item_quantity(
file: ifcopenshell.file,
cost_item: ifcopenshell.entity_instance,
products: list[ifcopenshell.entity_instance],
prop_name: str = "",
) -> None:
"""Adds a cost item quantity that is parametrically connected to a product
A cost item may have its subtotal calculated by multiplying a unit value
by a quantity associated with the cost item. That quantity may be either
manually specified or parametrically connected to a quantity on a
product. This API function lets you create that parametric connection.
For example, you may wish to have a cost item linked to the "NetVolume"
quantity on all IfcSlabs. Each quantity has a name which you can
specify. If the quantity is updated in-place (which should occur for
Native IFC applications) then the quantity for the cost item will
automatically update as well. If the quantity is deleted and then
re-added, then the parametric relationship is also lost.
This API also automatically assigns a control relationship between the
cost item and the product, so it is not necessary to use
ifcopenshell.api.control.assign_control.
If cost item has just 1 quantity and it's IfcQuantityCount, API will
assume that quantity is used for counting controlled objects
and it will recalculate the quantity value at the end of the API call.
:param cost_item: The IfcCostItem to assign parametric quantities to
:type cost_item: ifcopenshell.entity_instance
:param products: The IfcObjects to assign parametric quantities to
:type products: list[ifcopenshell.entity_instance]
:param prop_name: The name of the quantity. If this is not specified,
then it is assumed that there is no calculated quantity, and the
number of objects are counted instead.
:type prop_name: str, optional
:return: None
:rtype: None
Example:
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
# Let's imagine a unit cost of 5.0 per unit volume
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 5.0})
slab = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSlab")
# Usually the quantity would be automatically calculated via a
# graphical authoring application but let's assign a manual quantity
# for now.
qto = ifcopenshell.api.run("pset.add_qto", model, product=slab, name="Qto_SlabBaseQuantities")
ifcopenshell.api.run("pset.edit_qto", model, qto=qto, properties={"NetVolume": 42.0})
# Now let's parametrically link the slab's quantity to the cost
# item. If the slab is edited in the future and 42.0 changes, then
# the updated value will also automatically be applied to the cost
# item.
ifcopenshell.api.run("cost.assign_cost_item_quantity", model,
cost_item=item, products=[slab], prop_name="NetVolume")
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"cost_item": cost_item,
"products": products or [],
"prop_name": prop_name,
}
return usecase.execute()
class Usecase:
def __init__(self, file, cost_item=None, products=None, prop_name=""):
"""Adds a cost item quantity that is parametrically connected to a product
A cost item may have its subtotal calculated by multiplying a unit value
by a quantity associated with the cost item. That quantity may be either
manually specified or parametrically connected to a quantity on a
product. This API function lets you create that parametric connection.
For example, you may wish to have a cost item linked to the "NetVolume"
quantity on all IfcSlabs. Each quantity has a name which you can
specify. If the quantity is updated in-place (which should occur for
Native IFC applications) then the quantity for the cost item will
automatically update as well. If the quantity is deleted and then
re-added, then the parametric relationship is also lost.
This API also automatically assigns a control relationship between the
cost item and the product, so it is not necessary to use
ifcopenshell.api.control.assign_control.
:param cost_item: The IfcCostItem to assign parametric quantities to
:type cost_item: ifcopenshell.entity_instance.entity_instance
:param products: The IfcObjects to assign parametric quantities to
:type products: list[ifcopenshell.entity_instance.entity_instance]
:param prop_name: The name of the quantity. If this is not specified,
then it is assumed that there is no calculated quantity, and the
number of objects are counted instead.
:type prop_name: str, optional
:return: None
:rtype: None
Example:
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
# Let's imagine a unit cost of 5.0 per unit volume
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 5.0})
slab = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSlab")
# Usually the quantity would be automatically calculated via a
# graphical authoring application but let's assign a manual quantity
# for now.
qto = ifcopenshell.api.run("pset.add_qto", model, product=slab, name="Qto_SlabBaseQuantities")
ifcopenshell.api.run("pset.edit_qto", model, qto=qto, properties={"NetVolume": 42.0})
# Now let's parametrically link the slab's quantity to the cost
# item. If the slab is edited in the future and 42.0 changes, then
# the updated value will also automatically be applied to the cost
# item.
ifcopenshell.api.run("cost.assign_cost_item_quantity", model,
cost_item=item, products=[slab], prop_name="NetVolume")
"""
self.file = file
self.settings = {
"cost_item": cost_item,
"products": products or [],
"prop_name": prop_name,
}
file: ifcopenshell.file
settings: dict[str, Any]
def execute(self):
if self.settings["prop_name"]:
self.quantities = set(self.settings["cost_item"].CostQuantities or [])
for product in self.settings["products"]:
self.assign_cost_control(
related_object=product, cost_item=self.settings["cost_item"]
)
self.assign_cost_control(related_object=product, cost_item=self.settings["cost_item"])
if self.settings["prop_name"]:
if (
self.settings["cost_item"].CostQuantities
and self.settings["cost_item"].CostQuantities[0].Name.lower()
!= self.settings["prop_name"].lower()
and self.settings["cost_item"].CostQuantities[0].Name.lower() != self.settings["prop_name"].lower()
) or not product.is_a("IfcObject"):
continue
self.add_quantity_from_related_object(product)
@@ -103,7 +116,9 @@ class Usecase:
else:
self.update_cost_item_count()
def assign_cost_control(self, related_object, cost_item):
def assign_cost_control(
self, related_object: ifcopenshell.entity_instance, cost_item: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
return ifcopenshell.api.run(
"control.assign_control",
self.file,
@@ -111,19 +126,16 @@ class Usecase:
relating_control=cost_item,
)
def add_quantity_from_related_object(self, element):
def add_quantity_from_related_object(self, element: ifcopenshell.entity_instance) -> None:
for relationship in element.IsDefinedBy:
if relationship.is_a("IfcRelDefinesByProperties"):
self.add_quantity_from_qto(relationship.RelatingPropertyDefinition)
def add_quantity_from_qto(self, qto):
def add_quantity_from_qto(self, qto: ifcopenshell.entity_instance) -> None:
if not qto.is_a("IfcElementQuantity"):
return
for prop in qto.Quantities:
if (
prop.is_a("IfcPhysicalSimpleQuantity")
and prop.Name.lower() == self.settings["prop_name"].lower()
):
if prop.is_a("IfcPhysicalSimpleQuantity") and prop.Name.lower() == self.settings["prop_name"].lower():
self.quantities.add(prop)
def update_cost_item_count(self):
@@ -19,60 +19,59 @@
import ifcopenshell.api
class Usecase:
def __init__(self, file, cost_item=None, cost_rate=None):
"""Assigns a cost value to a cost item from a schedule of rates
def assign_cost_value(
file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance, cost_rate: ifcopenshell.entity_instance
) -> None:
"""Assigns a cost value to a cost item from a schedule of rates
Instead of assigning cost values from scratch for each cost item in a
cost schedule, the cost values may instead be assigned from a schedule
of rates.
Instead of assigning cost values from scratch for each cost item in a
cost schedule, the cost values may instead be assigned from a schedule
of rates.
A schedule of rates is just another cost schedule which have cost values
but no quantities. This API will allow you to "copy" the values from a
cost item in the schedule of rates into another cost item in your own
cost schedule. When the schedule of rates value is updated, then your
cost item values will also be updated. You can think of the schedule of
rates as a "template" to quickly populate your rates from.
A schedule of rates is just another cost schedule which have cost values
but no quantities. This API will allow you to "copy" the values from a
cost item in the schedule of rates into another cost item in your own
cost schedule. When the schedule of rates value is updated, then your
cost item values will also be updated. You can think of the schedule of
rates as a "template" to quickly populate your rates from.
:param cost_item: The IfcCostItem that you want to copy the values to
:type cost_item: ifcopenshell.entity_instance.entity_instance
:param cost_rate: The IfcCostItem that you want to copy the values from
:type cost_rate: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param cost_item: The IfcCostItem that you want to copy the values to
:type cost_item: ifcopenshell.entity_instance
:param cost_rate: The IfcCostItem that you want to copy the values from
:type cost_rate: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
# Let's create a schedule of rates with a single rate in it of 5.0
rate_tables = ifcopenshell.api.run("cost.add_cost_schedule", model,
predefined_type="SCHEDULEOFRATES")
rate = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=rate)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 5.0})
# Let's create a schedule of rates with a single rate in it of 5.0
rate_tables = ifcopenshell.api.run("cost.add_cost_schedule", model,
predefined_type="SCHEDULEOFRATES")
rate = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=rate)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 5.0})
# And this schedule will be for our actual cost plan / estimate / etc
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
# And this schedule will be for our actual cost plan / estimate / etc
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
# Now the cost item has the same rate as the one from the schedule of rate's item
ifcopenshell.api.run("cost.assign_cost_value", model, cost_item=item, cost_rate=rate)
"""
self.file = file
self.settings = {"cost_item": cost_item, "cost_rate": cost_rate}
# Now the cost item has the same rate as the one from the schedule of rate's item
ifcopenshell.api.run("cost.assign_cost_value", model, cost_item=item, cost_rate=rate)
"""
settings = {"cost_item": cost_item, "cost_rate": cost_rate}
def execute(self):
if self.settings["cost_item"].CostValues:
[
ifcopenshell.api.run(
"cost.remove_cost_value",
self.file,
parent=self.settings["cost_item"],
cost_value=cost_value,
)
for cost_value in self.settings["cost_item"].CostValues
]
# This is an assumption, and not part of the official IFC documentation
self.settings["cost_item"].CostValues = self.settings["cost_rate"].CostValues
if settings["cost_item"].CostValues:
[
ifcopenshell.api.run(
"cost.remove_cost_value",
file,
parent=settings["cost_item"],
cost_value=cost_value,
)
for cost_value in settings["cost_item"].CostValues
]
# This is an assumption, and not part of the official IFC documentation
settings["cost_item"].CostValues = settings["cost_rate"].CostValues
@@ -21,100 +21,97 @@ import ifcopenshell.util.date
import ifcopenshell.util.resource
class Usecase:
def __init__(self, file, cost_item=None):
"""Calculates the total cost of all resources associated with a cost item
def calculate_cost_item_resource_value(file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance) -> None:
"""Calculates the total cost of all resources associated with a cost item
A cost item may have construction resources (e.g. equipment, material,
etc) assigned to it. Construction resources may be assigned directly to
the cost item, or assigned first to a task, and the task is then
assigned to the cost item.
A cost item may have construction resources (e.g. equipment, material,
etc) assigned to it. Construction resources may be assigned directly to
the cost item, or assigned first to a task, and the task is then
assigned to the cost item.
The cost of a resource is calculated by the total sum of all of its base
costs. If no quantity is provided, that sum is considered to be the
total cost. Otherwise, it is considered to be a unit cost, and is then
multiplied by the resource quantity. The quantity is either stored as a
base quantity (such as a volume) for a things like material resources,
or as a duration as a daily rate for labour resources.
The cost of a resource is calculated by the total sum of all of its base
costs. If no quantity is provided, that sum is considered to be the
total cost. Otherwise, it is considered to be a unit cost, and is then
multiplied by the resource quantity. The quantity is either stored as a
base quantity (such as a volume) for a things like material resources,
or as a duration as a daily rate for labour resources.
The final calculated cost is set as the cost item's value. Any
previously existing values are removed.
The final calculated cost is set as the cost item's value. Any
previously existing values are removed.
:param cost_item: The IfcCostItem to calculate
:type cost_item: ifccopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param cost_item: The IfcCostItem to calculate
:type cost_item: ifccopenshell.entity_instance.entity_instance
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
# First, we need a cost schedule and item
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
# First, we need a cost schedule and item
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
# Let's imagine we have our own formworking crew
crew = ifcopenshell.api.run("resource.add_resource", model, ifc_class="IfcCrewResource")
# Let's imagine we have our own formworking crew
crew = ifcopenshell.api.run("resource.add_resource", model, ifc_class="IfcCrewResource")
# ... and they need concrete
concrete = ifcopenshell.api.run("resource.add_resource", model,
ifc_class="IfcConstructionMaterialResource", parent_resource=crew)
ifcopenshell.api.run("control.assign_control", model,
relating_control=item, related_object=concrete)
# ... which has a unit price of 42.0 per m3
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=concrete)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 42.0})
# ... and a volume of 200m3
quantity = ifcopenshell.api.run("resource.add_resource_quantity", model,
resource=concrete, ifc_class="IfcQuantityVolume")
ifcopenshell.api.run("resource.edit_resource_quantity", model,
physical_quantity=quantity, "attributes": {"VolumeValue": 200.0})
# ... and they need concrete
concrete = ifcopenshell.api.run("resource.add_resource", model,
ifc_class="IfcConstructionMaterialResource", parent_resource=crew)
ifcopenshell.api.run("control.assign_control", model,
relating_control=item, related_object=concrete)
# ... which has a unit price of 42.0 per m3
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=concrete)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 42.0})
# ... and a volume of 200m3
quantity = ifcopenshell.api.run("resource.add_resource_quantity", model,
resource=concrete, ifc_class="IfcQuantityVolume")
ifcopenshell.api.run("resource.edit_resource_quantity", model,
physical_quantity=quantity, "attributes": {"VolumeValue": 200.0})
# Let's say they also need some equipment
equipment = ifcopenshell.api.run("resource.add_resource", model,
ifc_class="IfcConstructionEquipmentResource", parent_resource=crew)
ifcopenshell.api.run("control.assign_control", model,
relating_control=item, related_object=equipment)
# ... with a fixed price of 50,000
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=concrete)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 42.0})
# Let's say they also need some equipment
equipment = ifcopenshell.api.run("resource.add_resource", model,
ifc_class="IfcConstructionEquipmentResource", parent_resource=crew)
ifcopenshell.api.run("control.assign_control", model,
relating_control=item, related_object=equipment)
# ... with a fixed price of 50,000
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=concrete)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 42.0})
# (42 * 200) + 50000 = 58400 is our calculated cost
ifcopenshell.api.run("cost.calculate_cost_item_resource_value", model, cost_item=item)
"""
self.file = file
self.settings = {"cost_item": cost_item}
# (42 * 200) + 50000 = 58400 is our calculated cost
ifcopenshell.api.run("cost.calculate_cost_item_resource_value", model, cost_item=item)
"""
settings = {"cost_item": cost_item}
def execute(self):
for cost_value in self.settings["cost_item"].CostValues or []:
ifcopenshell.api.run(
"cost.remove_cost_value", self.file, parent=self.settings["cost_item"], cost_value=cost_value
)
for cost_value in settings["cost_item"].CostValues or []:
ifcopenshell.api.run("cost.remove_cost_value", file, parent=settings["cost_item"], cost_value=cost_value)
resources = []
for rel in self.settings["cost_item"].Controls or []:
for related_object in rel.RelatedObjects:
if related_object.is_a("IfcConstructionResource"):
resources.append(related_object)
elif related_object.is_a("IfcTask"):
for rel2 in related_object.OperatesOn or []:
for related_object2 in rel2.RelatedObjects:
if related_object2.is_a("IfcConstructionResource"):
resources.append(related_object2)
resources = []
for rel in settings["cost_item"].Controls or []:
for related_object in rel.RelatedObjects:
if related_object.is_a("IfcConstructionResource"):
resources.append(related_object)
elif related_object.is_a("IfcTask"):
for rel2 in related_object.OperatesOn or []:
for related_object2 in rel2.RelatedObjects:
if related_object2.is_a("IfcConstructionResource"):
resources.append(related_object2)
for resource in resources:
cost, unit = ifcopenshell.util.resource.get_cost(resource)
if not cost:
cost, unit = ifcopenshell.util.resource.get_parent_cost(resource) # Concept to standardise - Not defined in schema, but this makes manual scheduling of resources 10x faster and less duplicate data.
quantity = ifcopenshell.util.resource.get_quantity(resource)
if not cost or not quantity:
continue
if unit and "day" in unit:
quantity = quantity / 8 # Assume 8 hour working day - TODO implement resource calendar
quantity = round(quantity, 2)
formula = "{}*{}".format(cost, quantity)
cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=self.settings["cost_item"])
cost_value.Name = resource.Name
ifcopenshell.api.run("cost.edit_cost_value_formula", self.file, cost_value=cost_value, formula=formula)
for resource in resources:
cost, unit = ifcopenshell.util.resource.get_cost(resource)
if not cost:
cost, unit = ifcopenshell.util.resource.get_parent_cost(
resource
) # Concept to standardise - Not defined in schema, but this makes manual scheduling of resources 10x faster and less duplicate data.
quantity = ifcopenshell.util.resource.get_quantity(resource)
if not cost or not quantity:
continue
if unit and "day" in unit:
quantity = quantity / 8 # Assume 8 hour working day - TODO implement resource calendar
quantity = round(quantity, 2)
formula = "{}*{}".format(cost, quantity)
cost_value = ifcopenshell.api.run("cost.add_cost_value", file, parent=settings["cost_item"])
cost_value.Name = resource.Name
ifcopenshell.api.run("cost.edit_cost_value_formula", file, cost_value=cost_value, formula=formula)
@@ -19,40 +19,48 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
from typing import Union
def copy_cost_item(
file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]:
# TODO: currently it never returns list of duplicated cost items
# though it is stated in the docs
"""Copies all cost items and related relationships
The following relationships are also duplicated:
* The copy will have the same attributes and property sets as the original cost item
* The copy will be assigned to the parent cost schedule
* The copy will have duplicated nested cost items
:param cost_item: The cost item to be duplicated
:type cost_item: ifcopenshell.entity_instance
:return: The duplicated cost item or the list of duplicated cost items if the latter has children
:rtype: ifcopenshell.entity_instance or list[ifcopenshell.entity_instance]
Example:
.. code:: python
# We have a cost item
cost_item = CostItem(name="Design new feature", deadline="2023-03-01")
# And now we have two
duplicated_cost_item = project.duplicate_cost_item(cost_item)
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {"cost_item": cost_item}
return usecase.execute()
class Usecase:
def __init__(self, file, cost_item=None):
"""Copies all cost items and related relationships
The following relationships are also duplicated:
* The copy will have the same attributes and property sets as the original cost item
* The copy will be assigned to the parent cost schedule
* The copy will have duplicated nested cost items
:param cost_item: The cost item to be duplicated
:type cost_item: ifcopenshell.entity_instance.entity_instance
:return: The duplicated cost item or the list of duplicated cost items if the latter has children
:rtype: ifcopenshell.entity_instance.entity_instance or list of ifcopenshell.entity_instance.entity_instance
Example:
.. code:: python
# We have a cost item
cost_item = CostItem(name="Design new feature", deadline="2023-03-01")
# And now we have two
duplicated_cost_item = project.duplicate_cost_item(cost_item)
"""
self.file = file
self.settings = {"cost_item": cost_item}
def execute(self):
self.new_cost_items = []
self.duplicate_cost_item(self.settings["cost_item"])
return self.duplicate_cost_item(self.settings["cost_item"])
def duplicate_cost_item(self, cost_item):
new_cost_item = ifcopenshell.util.element.copy_deep(self.file, cost_item)
@@ -20,45 +20,44 @@ import ifcopenshell.util.element
import ifcopenshell.api
class Usecase:
def __init__(self, file, source=None, destination=None):
"""Copies all cost values from one cost item to another
def copy_cost_item_values(
file: ifcopenshell.file, source: ifcopenshell.entity_instance, destination: ifcopenshell.entity_instance
) -> None:
"""Copies all cost values from one cost item to another
Any previously existing values will be removed. The entire value is
copied, including all components and formulas. However they are not
parametrically linked, so if one value changes, the other will not.
Any previously existing values will be removed. The entire value is
copied, including all components and formulas. However they are not
parametrically linked, so if one value changes, the other will not.
:param source: The IfcCostItem to copy cost values from
:type source: ifcopenshell.entity_instance.entity_instance
:param destination: The IfcCostItem to copy cost values from
:type destination: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param source: The IfcCostItem to copy cost values from
:type source: ifcopenshell.entity_instance
:param destination: The IfcCostItem to copy cost values from
:type destination: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
# Assume we have a schedule with multiple items in it
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item1 = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
item2 = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
# Assume we have a schedule with multiple items in it
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item1 = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
item2 = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
# One of the items has a value
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 5000.0})
# One of the items has a value
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 5000.0})
# Let's copy the value from one item to another
ifcopenshell.api.run("cost.copy_cost_item_values", model, source=item1, destination=item2)
"""
self.file = file
self.settings = {"source": source, "destination": destination}
# Let's copy the value from one item to another
ifcopenshell.api.run("cost.copy_cost_item_values", model, source=item1, destination=item2)
"""
settings = {"source": source, "destination": destination}
def execute(self):
for cost_value in self.settings["destination"].CostValues or []:
ifcopenshell.api.run("cost.remove_cost_item_value", self.file, cost_value=cost_value)
copied_cost_values = []
for cost_value in self.settings["source"].CostValues or []:
copied_cost_values.append(ifcopenshell.util.element.copy_deep(self.file, cost_value))
self.settings["destination"].CostValues = copied_cost_values
for cost_value in settings["destination"].CostValues or []:
ifcopenshell.api.run("cost.remove_cost_item_value", file, cost_value=cost_value)
copied_cost_values = []
for cost_value in settings["source"].CostValues or []:
copied_cost_values.append(ifcopenshell.util.element.copy_deep(file, cost_value))
settings["destination"].CostValues = copied_cost_values
@@ -15,33 +15,34 @@
#
# 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
from typing import Any
class Usecase:
def __init__(self, file, cost_item=None, attributes=None):
"""Edits the attributes of an IfcCostItem
def edit_cost_item(
file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcCostItem
For more information about the attributes and data types of an
IfcCostItem, consult the IFC documentation.
For more information about the attributes and data types of an
IfcCostItem, consult the IFC documentation.
:param cost_item: The IfcCostItem entity you want to edit
:type cost_item: ifcopenshell.entity_instance.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:return: None
:rtype: None
:param cost_item: The IfcCostItem entity you want to edit
:type cost_item: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
ifcopenshell.api.run("cost.edit_cost_item", model, cost_item=item, attributes={"Name": "Foo"})
"""
self.file = file
self.settings = {"cost_item": cost_item, "attributes": attributes or {}}
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
ifcopenshell.api.run("cost.edit_cost_item", model, cost_item=item, attributes={"Name": "Foo"})
"""
settings = {"cost_item": cost_item, "attributes": attributes or {}}
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["cost_item"], name, value)
for name, value in settings["attributes"].items():
setattr(settings["cost_item"], name, value)
@@ -15,41 +15,42 @@
#
# 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
from typing import Any
class Usecase:
def __init__(self, file, physical_quantity=None, attributes=None):
"""Edits the attributes of an IfcPhysicalQuantity
def edit_cost_item_quantity(
file: ifcopenshell.file, physical_quantity: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcPhysicalQuantity
For more information about the attributes and data types of an
IfcPhysicalQuantity, consult the IFC documentation.
For more information about the attributes and data types of an
IfcPhysicalQuantity, consult the IFC documentation.
:param physical_quantity: The IfcPhysicalQuantity entity you want to edit
:type physical_quantity: ifcopenshell.entity_instance.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:return: None
:rtype: None
:param physical_quantity: The IfcPhysicalQuantity entity you want to edit
:type physical_quantity: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
# This cost item will have a unit cost of 5 and a volume of 3
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 5.0})
quantity = ifcopenshell.api.run("cost.add_cost_item_quantity", model,
cost_item=item, ifc_class="IfcQuantityVolume")
ifcopenshell.api.run("cost.edit_cost_item_quantity", model,
physical_quantity=quantity, "attributes": {"VolumeValue": 3.0})
"""
self.file = file
self.settings = {"physical_quantity": physical_quantity, "attributes": attributes or {}}
# This cost item will have a unit cost of 5 and a volume of 3
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 5.0})
quantity = ifcopenshell.api.run("cost.add_cost_item_quantity", model,
cost_item=item, ifc_class="IfcQuantityVolume")
ifcopenshell.api.run("cost.edit_cost_item_quantity", model,
physical_quantity=quantity, "attributes": {"VolumeValue": 3.0})
"""
settings = {"physical_quantity": physical_quantity, "attributes": attributes or {}}
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["physical_quantity"], name, value)
for name, value in settings["attributes"].items():
setattr(settings["physical_quantity"], name, value)
@@ -15,34 +15,35 @@
#
# 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
from typing import Any
class Usecase:
def __init__(self, file, cost_schedule=None, attributes=None):
"""Edits the attributes of an IfcCostSchedule
def edit_cost_schedule(
file: ifcopenshell.file, cost_schedule: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcCostSchedule
For more information about the attributes and data types of an
IfcCostSchedule, consult the IFC documentation.
For more information about the attributes and data types of an
IfcCostSchedule, consult the IFC documentation.
:param cost_schedule: The IfcCostSchedule entity you want to edit
:type cost_schedule: ifcopenshell.entity_instance.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:return: None
:rtype: None
:param cost_schedule: The IfcCostSchedule entity you want to edit
:type cost_schedule: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
ifcopenshell.api.run("cost.edit_cost_schedule", model,
cost_schedule=schedule, attributes={"Name": "Foo"})
"""
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
ifcopenshell.api.run("cost.edit_cost_schedule", model,
cost_schedule=schedule, attributes={"Name": "Foo"})
"""
self.file = file
self.settings = {"cost_schedule": cost_schedule, "attributes": attributes or {}}
settings = {"cost_schedule": cost_schedule, "attributes": attributes or {}}
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["cost_schedule"], name, value)
for name, value in settings["attributes"].items():
setattr(settings["cost_schedule"], name, value)
@@ -19,50 +19,50 @@
import ifcopenshell
import ifcopenshell.util.unit
import ifcopenshell.util.element
from typing import Any
class Usecase:
def __init__(self, file, cost_value=None, attributes=None):
"""Edits the attributes of an IfcCostValue
def edit_cost_value(
file: ifcopenshell.file, cost_value: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcCostValue
For more information about the attributes and data types of an
IfcCostValue, consult the IFC documentation.
For more information about the attributes and data types of an
IfcCostValue, consult the IFC documentation.
:param cost_value: The IfcCostValue entity you want to edit
:type cost_value: ifcopenshell.entity_instance.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:return: None
:rtype: None
:param cost_value: The IfcCostValue entity you want to edit
:type cost_value: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
# This cost item will have a total cost of 42
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 42.0})
"""
self.file = file
self.settings = {"cost_value": cost_value, "attributes": attributes or {}}
# This cost item will have a total cost of 42
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 42.0})
"""
settings = {"cost_value": cost_value, "attributes": attributes or {}}
def execute(self):
for name, value in self.settings["attributes"].items():
if name == "AppliedValue" and value is not None:
# TODO: support all applied value select types
value = self.file.createIfcMonetaryMeasure(value)
elif name == "UnitBasis":
old_unit_basis = self.settings["cost_value"].UnitBasis
if value:
value_component = self.file.create_entity(
ifcopenshell.util.unit.get_unit_measure_class(value["UnitComponent"].UnitType),
value["ValueComponent"],
)
value = self.file.create_entity("IfcMeasureWithUnit", value_component, value["UnitComponent"])
if old_unit_basis and len(self.file.get_inverse(old_unit_basis)) == 0:
ifcopenshell.util.element.remove_deep(self.file, old_unit_basis)
setattr(self.settings["cost_value"], name, value)
for name, value in settings["attributes"].items():
if name == "AppliedValue" and value is not None:
# TODO: support all applied value select types
value = file.createIfcMonetaryMeasure(value)
elif name == "UnitBasis":
old_unit_basis = settings["cost_value"].UnitBasis
if value:
value_component = file.create_entity(
ifcopenshell.util.unit.get_unit_measure_class(value["UnitComponent"].UnitType),
value["ValueComponent"],
)
value = file.create_entity("IfcMeasureWithUnit", value_component, value["UnitComponent"])
if old_unit_basis and len(file.get_inverse(old_unit_basis)) == 0:
ifcopenshell.util.element.remove_deep(file, old_unit_basis)
setattr(settings["cost_value"], name, value)
@@ -17,42 +17,46 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.cost
import ifcopenshell.util.unit
import ifcopenshell.util.element
def edit_cost_value_formula(file: ifcopenshell.file, cost_value: ifcopenshell.entity_instance, formula: str) -> None:
"""Sets a cost value based on a formula, similar to formulas in spreadsheets
Costs may be made up of many components (e.g. labour, material, waste
factor, taxes, etc). This can be easily represented in the form of a
formula similar thta would be used in spreadsheet applications.
For more information, see ifcopenshell.util.cost
:param cost_value: The IfcCostValue to set the values of
:type cost_value: ifcopenshell.entity_instance
:param formula: The formula following the language of ifcopenshell.util.cost
:type formula: str
:return: None
:rtype: None
Example:
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item)
ifcopenshell.api.run("cost.edit_cost_value_formula", model, cost_value=value,
formula="5000 * 1.19")
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {"cost_value": cost_value, "formula": formula or {}}
return usecase.execute()
class Usecase:
def __init__(self, file, cost_value=None, formula=None):
"""Sets a cost value based on a formula, similar to formulas in spreadsheets
Costs may be made up of many components (e.g. labour, material, waste
factor, taxes, etc). This can be easily represented in the form of a
formula similar thta would be used in spreadsheet applications.
For more information, see ifcopenshell.util.cost
:param cost_value: The IfcCostValue to set the values of
:type cost_value: ifcopenshell.entity_instance.entity_instance
:param formula: The formula following the language of ifcopenshell.util.cost
:type formula: str
:return: None
:rtype: None
Example:
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item)
ifcopenshell.api.run("cost.edit_cost_value_formula", model, cost_value=value,
formula="5000 * 1.19")
"""
self.file = file
self.settings = {"cost_value": cost_value, "formula": formula or {}}
def execute(self):
try:
data = ifcopenshell.util.cost.unserialise_cost_value(self.settings["formula"], self.settings["cost_value"])
@@ -21,48 +21,45 @@ import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, cost_item=None):
"""Removes a cost item
def remove_cost_item(file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance) -> None:
"""Removes a cost item
All associated relationships with the cost item are also removed,
however the related resources, products, and tasks themselves are
retained.
All associated relationships with the cost item are also removed,
however the related resources, products, and tasks themselves are
retained.
:param cost_item: The IfcCostItem entity you want to remove
:type cost_item: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param cost_item: The IfcCostItem entity you want to remove
:type cost_item: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
ifcopenshell.api.run("cost.remove_cost_item", model, cost_item=item)
"""
self.file = file
self.settings = {"cost_item": cost_item}
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
ifcopenshell.api.run("cost.remove_cost_item", model, cost_item=item)
"""
settings = {"cost_item": cost_item}
def execute(self):
# TODO: do a deep purge
for inverse in self.file.get_inverse(self.settings["cost_item"]):
if inverse.is_a("IfcRelNests"):
if inverse.RelatingObject == self.settings["cost_item"]:
for related_object in inverse.RelatedObjects:
ifcopenshell.api.run("cost.remove_cost_item", self.file, cost_item=related_object)
elif inverse.RelatedObjects == (self.settings["cost_item"],):
history = inverse.OwnerHistory
self.file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
elif inverse.is_a("IfcRelAssignsToControl"):
# TODO: do a deep purge
for inverse in file.get_inverse(settings["cost_item"]):
if inverse.is_a("IfcRelNests"):
if inverse.RelatingObject == settings["cost_item"]:
for related_object in inverse.RelatedObjects:
ifcopenshell.api.run("cost.remove_cost_item", file, cost_item=related_object)
elif inverse.RelatedObjects == (settings["cost_item"],):
history = inverse.OwnerHistory
self.file.remove(inverse)
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
history = self.settings["cost_item"].OwnerHistory
self.file.remove(self.settings["cost_item"])
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
ifcopenshell.util.element.remove_deep2(file, history)
elif inverse.is_a("IfcRelAssignsToControl"):
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
history = settings["cost_item"].OwnerHistory
file.remove(settings["cost_item"])
if history:
ifcopenshell.util.element.remove_deep2(file, history)
@@ -15,42 +15,42 @@
#
# 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
class Usecase:
def __init__(self, file, cost_item=None, physical_quantity=None):
"""Removes a quantity assigned to a cost item
def remove_cost_item_quantity(
file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance, physical_quantity: ifcopenshell.entity_instance
) -> None:
"""Removes a quantity assigned to a cost item
If the quantity is part of a product (e.g. wall), then the quantity will
still exist and merely the relationship to the cost item will be
removed.
If the quantity is part of a product (e.g. wall), then the quantity will
still exist and merely the relationship to the cost item will be
removed.
:param cost_item: The IfcCostItem that the quantity is assigned to
:type cost_item: ifcopenshell.entity_instance.entity_instance
:param physical_quantity: The IfcPhysicalQuantity to remove
:type physical_quantity: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param cost_item: The IfcCostItem that the quantity is assigned to
:type cost_item: ifcopenshell.entity_instance
:param physical_quantity: The IfcPhysicalQuantity to remove
:type physical_quantity: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
quantity = ifcopenshell.api.run("cost.add_cost_item_quantity", model,
cost_item=item, ifc_class="IfcQuantityVolume")
# Let's change our mind and delete it
ifcopenshell.api.run("cost.remove_cost_item", model,
cost_item=item, physical_quantity=quantity)
"""
self.file = file
self.settings = {"cost_item": cost_item, "physical_quantity": physical_quantity}
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
quantity = ifcopenshell.api.run("cost.add_cost_item_quantity", model,
cost_item=item, ifc_class="IfcQuantityVolume")
# Let's change our mind and delete it
ifcopenshell.api.run("cost.remove_cost_item", model,
cost_item=item, physical_quantity=quantity)
"""
settings = {"cost_item": cost_item, "physical_quantity": physical_quantity}
def execute(self):
if len(self.file.get_inverse(self.settings["physical_quantity"])) == 1:
self.file.remove(self.settings["physical_quantity"])
return
quantities = list(self.settings["cost_item"].CostQuantities or [])
quantities.remove(self.settings["physical_quantity"])
self.settings["cost_item"].CostQuantities = quantities
if len(file.get_inverse(settings["physical_quantity"])) == 1:
file.remove(settings["physical_quantity"])
return
quantities = list(settings["cost_item"].CostQuantities or [])
quantities.remove(settings["physical_quantity"])
settings["cost_item"].CostQuantities = quantities
@@ -21,41 +21,36 @@ import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, cost_schedule=None):
"""Removes a cost schedule
def remove_cost_schedule(file: ifcopenshell.file, cost_schedule: ifcopenshell.entity_instance) -> None:
"""Removes a cost schedule
All associated relationships with the cost schedule are also removed,
including all cost items.
All associated relationships with the cost schedule are also removed,
including all cost items.
:param cost_schedule: The IfcCostSchedule entity you want to remove
:type cost_schedule: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param cost_schedule: The IfcCostSchedule entity you want to remove
:type cost_schedule: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
ifcopenshell.api.run("cost.remove_cost_schedule", model, cost_schedule=schedule)
"""
self.file = file
self.settings = {"cost_schedule": cost_schedule}
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
ifcopenshell.api.run("cost.remove_cost_schedule", model, cost_schedule=schedule)
"""
settings = {"cost_schedule": cost_schedule}
def execute(self):
# TODO: do a deep purge
for inverse in self.file.get_inverse(self.settings["cost_schedule"]):
if inverse.is_a("IfcRelAssignsToControl"):
[
ifcopenshell.api.run(
"cost.remove_cost_item", self.file, cost_item=related_object
)
for related_object in inverse.RelatedObjects
if related_object.is_a("IfcCostItem")
]
history = self.settings["cost_schedule"].OwnerHistory
self.file.remove(self.settings["cost_schedule"])
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
# TODO: do a deep purge
for inverse in file.get_inverse(settings["cost_schedule"]):
if inverse.is_a("IfcRelAssignsToControl"):
[
ifcopenshell.api.run("cost.remove_cost_item", file, cost_item=related_object)
for related_object in inverse.RelatedObjects
if related_object.is_a("IfcCostItem")
]
history = settings["cost_schedule"].OwnerHistory
file.remove(settings["cost_schedule"])
if history:
ifcopenshell.util.element.remove_deep2(file, history)
@@ -15,53 +15,53 @@
#
# 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
class Usecase:
def __init__(self, file, parent=None, cost_value=None):
"""Removes a cost value
def remove_cost_value(
file: ifcopenshell.file, parent: ifcopenshell.entity_instance, cost_value: ifcopenshell.entity_instance
) -> None:
"""Removes a cost value
The cost value may be assigned either to a cost item, a construction
resource, or another cost value (i.e. it is a subcomponent of a cost)
The cost value may be assigned either to a cost item, a construction
resource, or another cost value (i.e. it is a subcomponent of a cost)
:param parent: The IfcCostItem, IfcConstructionResource, or IfcCostValue
that the IfcCostValue is assigned to.
:type parent: ifcopenshell.entity_instance.entity_instance
:param cost_value: The IfcCostValue that you want to remove
:type parent: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param parent: The IfcCostItem, IfcConstructionResource, or IfcCostValue
that the IfcCostValue is assigned to.
:type parent: ifcopenshell.entity_instance
:param cost_value: The IfcCostValue that you want to remove
:type parent: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
# This cost item will have a unit cost of 5 and a volume of 3
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 5.0})
# This cost item will have a unit cost of 5 and a volume of 3
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 5.0})
ifcopenshell.api.run("cost.remove_cost_value", model, parent=item, cost_value=value)
"""
self.file = file
self.settings = {"parent": parent, "cost_value": cost_value}
ifcopenshell.api.run("cost.remove_cost_value", model, parent=item, cost_value=value)
"""
settings = {"parent": parent, "cost_value": cost_value}
def execute(self):
if len(self.file.get_inverse(self.settings["cost_value"])) == 1:
self.file.remove(self.settings["cost_value"])
# TODO deep purge
elif self.settings["parent"].is_a("IfcCostItem"):
values = list(self.settings["parent"].CostValues)
values.remove(self.settings["cost_value"])
self.settings["parent"].CostValues = values if values else None
elif self.settings["parent"].is_a("IfcConstructionResource"):
values = list(self.settings["parent"].BaseCosts)
values.remove(self.settings["cost_value"])
self.settings["parent"].BaseCosts = values if values else None
elif self.settings["parent"].is_a("IfcCostValue"):
components = list(self.settings["parent"].Components)
components.remove(self.settings["cost_value"])
self.settings["parent"].Components = components if components else None
if len(file.get_inverse(settings["cost_value"])) == 1:
file.remove(settings["cost_value"])
# TODO deep purge
elif settings["parent"].is_a("IfcCostItem"):
values = list(settings["parent"].CostValues)
values.remove(settings["cost_value"])
settings["parent"].CostValues = values if values else None
elif settings["parent"].is_a("IfcConstructionResource"):
values = list(settings["parent"].BaseCosts)
values.remove(settings["cost_value"])
settings["parent"].BaseCosts = values if values else None
elif settings["parent"].is_a("IfcCostValue"):
components = list(settings["parent"].Components)
components.remove(settings["cost_value"])
settings["parent"].Components = components if components else None
@@ -19,56 +19,61 @@
import ifcopenshell.api
def unassign_cost_item_quantity(
file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance, products: list[ifcopenshell.entity_instance]
) -> None:
"""Removes quantities of a cost item that are calculated on products
A cost item may have quantities that are parametrically calculated on
physical products. This lets you remove those quantities. This means
that any future changes in the physical product's dimensions will not
have any impact on the cost item.
:param cost_item: The IfcCostItem to remove quantities from
:type cost_item: ifcopenshell.entity_instance
:param products: A list of IfcProducts that may have parametrically
connected quantities to the cost item
:type products: list[ifcopenshell.entity_instance]
:return: None
:rtype: None
Example:
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
# Let's imagine a unit cost of 5.0 per unit volume
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 5.0})
slab = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSlab")
# Usually the quantity would be automatically calculated via a
# graphical authoring application but let's assign a manual quantity
# for now.
qto = ifcopenshell.api.run("pset.add_qto", model, product=slab, name="Qto_SlabBaseQuantities")
ifcopenshell.api.run("pset.edit_qto", model, qto=qto, properties={"NetVolume": 42.0})
# Now let's parametrically link the slab's quantity to the cost
# item. If the slab is edited in the future and 42.0 changes, then
# the updated value will also automatically be applied to the cost
# item.
ifcopenshell.api.run("cost.assign_cost_item_quantity", model,
cost_item=item, products=[slab], prop_name="NetVolume")
# Let's change our mind and remove the parametric connection
ifcopenshell.api.run("cost.unassign_cost_item_quantity", model,
cost_item=item, products=[slab])
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {"cost_item": cost_item, "products": products or []}
return usecase.execute()
class Usecase:
def __init__(self, file, cost_item=None, products=None):
"""Removes quantities of a cost item that are calculated on products
A cost item may have quantities that are parametrically calculated on
physical products. This lets you remove those quantities. This means
that any future changes in the physical product's dimensions will not
have any impact on the cost item.
:param cost_item: The IfcCostItem to remove quantities from
:type cost_item: ifcopenshell.entity_instance.entity_instance
:param products: A list of IfcProducts that may have parametrically
connected quantities to the cost item
:type products: list[ifcopenshell.entity_instance.entity_instance]
:return: None
:rtype: None
Example:
.. code:: python
schedule = ifcopenshell.api.run("cost.add_cost_schedule", model)
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
# Let's imagine a unit cost of 5.0 per unit volume
value = ifcopenshell.api.run("cost.add_cost_value", model, parent=item)
ifcopenshell.api.run("cost.edit_cost_value", model, cost_value=value,
attributes={"AppliedValue": 5.0})
slab = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSlab")
# Usually the quantity would be automatically calculated via a
# graphical authoring application but let's assign a manual quantity
# for now.
qto = ifcopenshell.api.run("pset.add_qto", model, product=slab, name="Qto_SlabBaseQuantities")
ifcopenshell.api.run("pset.edit_qto", model, qto=qto, properties={"NetVolume": 42.0})
# Now let's parametrically link the slab's quantity to the cost
# item. If the slab is edited in the future and 42.0 changes, then
# the updated value will also automatically be applied to the cost
# item.
ifcopenshell.api.run("cost.assign_cost_item_quantity", model,
cost_item=item, products=[slab], prop_name="NetVolume")
# Let's change our mind and remove the parametric connection
ifcopenshell.api.run("cost.unassign_cost_item_quantity", model,
cost_item=item, products=[slab])
"""
self.file = file
self.settings = {"cost_item": cost_item, "products": products or []}
def execute(self):
self.quantities = set(self.settings["cost_item"].CostQuantities or [])
for quantity in self.settings["cost_item"].CostQuantities or []:
@@ -15,3 +15,34 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Reference external project documents and associate them to model elements
Some project information (drawings, specifications, certificates, reports, etc)
may be stored in external documents (locally or in a CDE). IFC lets you store a
register of documents with metadata and associate them with elements (both
physical and non-physical).
"""
from .. import wrap_usecases
from .add_information import add_information
from .add_reference import add_reference
from .assign_document import assign_document
from .edit_information import edit_information
from .edit_reference import edit_reference
from .remove_information import remove_information
from .remove_reference import remove_reference
from .unassign_document import unassign_document
wrap_usecases(__path__, __name__)
__all__ = [
"add_information",
"add_reference",
"assign_document",
"edit_information",
"edit_reference",
"remove_information",
"remove_reference",
"unassign_document",
]
@@ -17,71 +17,65 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.guid
from typing import Optional
class Usecase:
def __init__(self, file, parent=None):
"""Adds a new document information to the project
def add_information(
file: ifcopenshell.file, parent: Optional[ifcopenshell.entity_instance] = None
) -> ifcopenshell.entity_instance:
"""Adds a new document information to the project
An IFC document information is a document associated with the project.
It may be a drawing, specification, schedule, certificate, warranty
guarantee, manual, contract, and so on. They are often used for drawings
and facility management purposes.
An IFC document information is a document associated with the project.
It may be a drawing, specification, schedule, certificate, warranty
guarantee, manual, contract, and so on. They are often used for drawings
and facility management purposes.
A document may also be a subdocument of a larger document, this is
useful for superseding documents or tracking older versions. The parent
is considered the latest version and the children are older revisions.
A document may also be a subdocument of a larger document, this is
useful for superseding documents or tracking older versions. The parent
is considered the latest version and the children are older revisions.
:param parent: The parent document, if necessary.
:type parent: ifcopenshell.entity_instance.entity_instance, optional
:return: The newly created IfcDocumentInformation entity
:rtype: ifcopenshell.entity_instance.entity_instance
:param parent: The parent document, if necessary.
:type parent: ifcopenshell.entity_instance, optional
:return: The newly created IfcDocumentInformation entity
:rtype: ifcopenshell.entity_instance
Example:
Example:
.. code:: python
.. code:: python
document = ifcopenshell.api.run("document.add_information", model)
# A document typically has a unique drawing or document name (which
# follows a coding system depending on the project), as well as a
# title. This should match what is shown on the titleblock or title
# page of the document. At a minimum you'd also want to specify a
# URI location. The location may be on local, or on a CDE, or any
# other platform.
ifcopenshell.api.run("document.edit_information", model,
information=document,
attributes={"Identification": "A-GA-6100", "Name": "Overall Plan",
"Location": "A-GA-6100 - Overall Plan.pdf"})
"""
self.file = file
self.settings = {"parent": parent}
def execute(self):
id_attribute = "DocumentId" if self.file.schema == "IFC2X3" else "Identification"
information = self.file.create_entity(
"IfcDocumentInformation", **{id_attribute: "X", "Name": "Unnamed"}
document = ifcopenshell.api.run("document.add_information", model)
# A document typically has a unique drawing or document name (which
# follows a coding system depending on the project), as well as a
# title. This should match what is shown on the titleblock or title
# page of the document. At a minimum you'd also want to specify a
# URI location. The location may be on local, or on a CDE, or any
# other platform.
ifcopenshell.api.run("document.edit_information", model,
information=document,
attributes={"Identification": "A-GA-6100", "Name": "Overall Plan",
"Location": "A-GA-6100 - Overall Plan.pdf"})
"""
id_attribute = "DocumentId" if file.schema == "IFC2X3" else "Identification"
information = file.create_entity("IfcDocumentInformation", **{id_attribute: "X", "Name": "Unnamed"})
if not parent and file.by_type("IfcProject"):
parent = file.by_type("IfcProject")[0]
if parent.is_a("IfcProject") or parent.is_a("IfcContext"):
file.create_entity(
"IfcRelAssociatesDocument",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", file),
RelatingDocument=information,
RelatedObjects=[parent],
)
parent = self.settings["parent"]
if not parent and self.file.by_type("IfcProject"):
parent = self.file.by_type("IfcProject")[0]
if parent.is_a("IfcProject") or parent.is_a("IfcContext"):
self.file.create_entity(
"IfcRelAssociatesDocument",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file),
RelatingDocument=information,
RelatedObjects=[parent],
elif parent.is_a("IfcDocumentInformation"):
if parent.IsPointer:
rel = parent.IsPointer[0]
documents = set(rel.RelatedDocuments)
documents.add(information)
rel.RelatedDocuments = list(documents)
else:
file.create_entity(
"IfcDocumentInformationRelationship", RelatingDocument=parent, RelatedDocuments=[information]
)
elif parent.is_a("IfcDocumentInformation"):
if parent.IsPointer:
rel = parent.IsPointer[0]
documents = set(rel.RelatedDocuments)
documents.add(information)
rel.RelatedDocuments = list(documents)
else:
self.file.create_entity(
"IfcDocumentInformationRelationship",
RelatingDocument=parent,
RelatedDocuments=[information]
)
return information
return information
@@ -19,62 +19,57 @@
import ifcopenshell
class Usecase:
def __init__(self, file: ifcopenshell.file, information: ifcopenshell.entity_instance):
"""Creates a new reference to a document to assign to products
def add_reference(file: ifcopenshell.file, information: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
"""Creates a new reference to a document to assign to products
A document may be associated with physical products, tasks, cost items,
and so on. For example, spaces, storeys, and buildings may have a list
of associated drawings so you can see which drawings (e.g. plans,
sections, details) are documenting that location. Alternatively,
equipment may have associated training manuals, operation and
maintenance manuals or detailed assembly drawings. Resources may be
training certification required, schedules may have gantt charts or bid
documents, and so on.
A document may be associated with physical products, tasks, cost items,
and so on. For example, spaces, storeys, and buildings may have a list
of associated drawings so you can see which drawings (e.g. plans,
sections, details) are documenting that location. Alternatively,
equipment may have associated training manuals, operation and
maintenance manuals or detailed assembly drawings. Resources may be
training certification required, schedules may have gantt charts or bid
documents, and so on.
In order to associate a document with an object, a reference to that
document needs to be created. It could be a reference to the entire
document, or a reference to a particular page or chapter. See
ifcopenshell.api.document.assign_document for more information.
In order to associate a document with an object, a reference to that
document needs to be created. It could be a reference to the entire
document, or a reference to a particular page or chapter. See
ifcopenshell.api.document.assign_document for more information.
:param information: The IfcDocumentInformation that the reference will
be created for
:type information: ifcopenshell.entity_instance.entity_instance
:return: The newly created IfcDocumentReference entity
:rtype: ifcopenshell.entity_instance.entity_instance
:param information: The IfcDocumentInformation that the reference will
be created for
:type information: ifcopenshell.entity_instance
:return: The newly created IfcDocumentReference entity
:rtype: ifcopenshell.entity_instance
Example:
Example:
.. code:: python
.. code:: python
document = ifcopenshell.api.run("document.add_information", model)
ifcopenshell.api.run("document.edit_information", model,
information=document,
attributes={"Identification": "A-GA-6100", "Name": "Overall Plan",
"Location": "A-GA-6100 - Overall Plan.pdf"})
document = ifcopenshell.api.run("document.add_information", model)
ifcopenshell.api.run("document.edit_information", model,
information=document,
attributes={"Identification": "A-GA-6100", "Name": "Overall Plan",
"Location": "A-GA-6100 - Overall Plan.pdf"})
# In this case, we don't specify any more information, and so the
# reference is for the entire document, as opposed to a single page or
# chapter or section.
reference = ifcopenshell.api.run("document.add_reference", model, information=document)
# In this case, we don't specify any more information, and so the
# reference is for the entire document, as opposed to a single page or
# chapter or section.
reference = ifcopenshell.api.run("document.add_reference", model, information=document)
# Alternatively, we can specify a single section, such as by a
# subheading code.
reference2 = ifcopenshell.api.run("document.add_reference", model, information=document)
ifcopenshell.api.run("document.edit_reference", model,
reference=reference2, attributes={"Identification": "2.1.15"})
"""
self.file = file
self.settings = {"information": information}
# Alternatively, we can specify a single section, such as by a
# subheading code.
reference2 = ifcopenshell.api.run("document.add_reference", model, information=document)
ifcopenshell.api.run("document.edit_reference", model,
reference=reference2, attributes={"Identification": "2.1.15"})
"""
settings = {"information": information}
def execute(self) -> ifcopenshell.entity_instance:
if self.file.schema == "IFC2X3":
reference = self.file.create_entity("IfcDocumentReference", ItemReference="X")
if self.settings["information"]:
references = list(self.settings["information"].DocumentReferences or [])
references.append(reference)
self.settings["information"].DocumentReferences = references
return reference
return self.file.create_entity(
"IfcDocumentReference", ReferencedDocument=self.settings["information"], Identification="X"
)
if file.schema == "IFC2X3":
reference = file.create_entity("IfcDocumentReference", ItemReference="X")
if settings["information"]:
references = list(settings["information"].DocumentReferences or [])
references.append(reference)
settings["information"].DocumentReferences = references
return reference
return file.create_entity("IfcDocumentReference", ReferencedDocument=settings["information"], Identification="X")
@@ -18,97 +18,90 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.element
from typing import Union
class Usecase:
def __init__(
self,
file: ifcopenshell.file,
products: list[ifcopenshell.entity_instance],
document: ifcopenshell.entity_instance,
):
"""Assigns a document to a list of products
def assign_document(
file: ifcopenshell.file,
products: list[ifcopenshell.entity_instance],
document: ifcopenshell.entity_instance,
) -> Union[ifcopenshell.entity_instance, None]:
"""Assigns a document to a list of products
An object may be assigned to zero, one, or multiple documents. Almost
any object or property may be assigned to a document, though typically
we'd only use it for spaces, types, physical products and schedules.
Adding a new assignment is typically done using a document reference and
an object. IFC technically allows association with a document
information and an object, but this is not encouraged because it is not
consistent with other external relationships (such as classification
systems or libraries).
An object may be assigned to zero, one, or multiple documents. Almost
any object or property may be assigned to a document, though typically
we'd only use it for spaces, types, physical products and schedules.
Adding a new assignment is typically done using a document reference and
an object. IFC technically allows association with a document
information and an object, but this is not encouraged because it is not
consistent with other external relationships (such as classification
systems or libraries).
:param product: The list of objects to associate the document to. This could be
almost any sensible object in IFC.
:type product: list[ifcopenshell.entity_instance.entity_instance]
:param document: The IfcDocumentReference to associate to, or
alternatively an IfcDocumentInformation, though this is not
recommended.
:type document: ifcopenshell.entity_instance.entity_instance
:return: The IfcRelAssociatesDocument relationship
or `None` if `products` was an empty list or all products were
already assigned to the `document`.
:rtype: ifcopenshell.entity_instance.entity_instance
:param product: The list of objects to associate the document to. This could be
almost any sensible object in IFC.
:type product: list[ifcopenshell.entity_instance]
:param document: The IfcDocumentReference to associate to, or
alternatively an IfcDocumentInformation, though this is not
recommended.
:type document: ifcopenshell.entity_instance
:return: The IfcRelAssociatesDocument relationship
or `None` if `products` was an empty list or all products were
already assigned to the `document`.
:rtype: ifcopenshell.entity_instance
Example:
Example:
.. code:: python
.. code:: python
document = ifcopenshell.api.run("document.add_information", model)
ifcopenshell.api.run("document.edit_information", model,
information=document,
attributes={"Identification": "A-GA-6100", "Name": "Overall Plan",
"Location": "A-GA-6100 - Overall Plan.pdf"})
reference = ifcopenshell.api.run("document.add_reference", model, information=document)
document = ifcopenshell.api.run("document.add_information", model)
ifcopenshell.api.run("document.edit_information", model,
information=document,
attributes={"Identification": "A-GA-6100", "Name": "Overall Plan",
"Location": "A-GA-6100 - Overall Plan.pdf"})
reference = ifcopenshell.api.run("document.add_reference", model, information=document)
# Let's imagine storey represents an IfcBuildingStorey for the ground floor
ifcopenshell.api.run("document.assign_document", model, products=[storey], document=reference)
"""
self.file = file
self.settings = {
"products": products,
"document": document,
}
# Let's imagine storey represents an IfcBuildingStorey for the ground floor
ifcopenshell.api.run("document.assign_document", model, products=[storey], document=reference)
"""
settings = {
"products": products,
"document": document,
}
def execute(self) -> Union[ifcopenshell.entity_instance, None]:
# TODO: do we need to support non-ifcroot elements like we do in classification.add_reference?
# NOTE: reuses code from `library.assign_reference`
# TODO: do we need to support non-ifcroot elements like we do in classification.add_reference?
# NOTE: reuses code from `library.assign_reference`
referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["document"])
products: set[ifcopenshell.entity_instance] = set(self.settings["products"])
products = products - referenced_elements
referenced_elements = ifcopenshell.util.element.get_referenced_elements(settings["document"])
products: set[ifcopenshell.entity_instance] = set(settings["products"])
products = products - referenced_elements
if not products:
return
if not products:
return
if self.file.schema == "IFC2X3":
rel = next(
(
r
for r in self.file.by_type("IfcRelAssociatesDocument")
if r.RelatingDocument == self.settings["document"]
),
None,
)
else:
ifc_class = self.settings["document"].is_a()
if ifc_class == "IfcDocumentReference":
rel = next(iter(self.settings["document"].DocumentRefForObjects), None)
elif ifc_class == "IfcDocumentInformation":
rel = next(iter(self.settings["document"].DocumentInfoForObjects), None)
if file.schema == "IFC2X3":
rel = next(
(r for r in file.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == settings["document"]),
None,
)
else:
ifc_class = settings["document"].is_a()
if ifc_class == "IfcDocumentReference":
rel = next(iter(settings["document"].DocumentRefForObjects), None)
elif ifc_class == "IfcDocumentInformation":
rel = next(iter(settings["document"].DocumentInfoForObjects), None)
if not rel:
return self.file.create_entity(
"IfcRelAssociatesDocument",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file),
RelatedObjects=list(products),
RelatingDocument=self.settings["document"],
)
if not rel:
return file.create_entity(
"IfcRelAssociatesDocument",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", file),
RelatedObjects=list(products),
RelatingDocument=settings["document"],
)
related_objects = set(rel.RelatedObjects) | products
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", self.file, element=rel)
return rel
related_objects = set(rel.RelatedObjects) | products
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", file, element=rel)
return rel
@@ -15,35 +15,38 @@
#
# 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
from typing import Any
class Usecase:
def __init__(self, file, information=None, attributes=None):
"""Edits the attributes of an IfcDocumentInformation
def edit_information(
file: ifcopenshell.file,
information: ifcopenshell.entity_instance,
attributes: dict[str, Any],
) -> None:
"""Edits the attributes of an IfcDocumentInformation
For more information about the attributes and data types of an
IfcDocumentInformation, consult the IFC documentation.
For more information about the attributes and data types of an
IfcDocumentInformation, consult the IFC documentation.
:param reference: The IfcDocumentInformation entity you want to edit
:type reference: ifcopenshell.entity_instance.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:return: None
:rtype: None
:param reference: The IfcDocumentInformation entity you want to edit
:type reference: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
document = ifcopenshell.api.run("document.add_information", model)
ifcopenshell.api.run("document.edit_information", model,
information=document,
attributes={"Identification": "A-GA-6100", "Name": "Overall Plan",
"Location": "A-GA-6100 - Overall Plan.pdf"})
"""
self.file = file
self.settings = {"information": information, "attributes": attributes or {}}
document = ifcopenshell.api.run("document.add_information", model)
ifcopenshell.api.run("document.edit_information", model,
information=document,
attributes={"Identification": "A-GA-6100", "Name": "Overall Plan",
"Location": "A-GA-6100 - Overall Plan.pdf"})
"""
settings = {"information": information, "attributes": attributes}
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["information"], name, value)
for name, value in settings["attributes"].items():
setattr(settings["information"], name, value)
@@ -15,38 +15,41 @@
#
# 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
from typing import Any
class Usecase:
def __init__(self, file, reference=None, attributes=None):
"""Edits the attributes of an IfcDocumentReference
def edit_reference(
file: ifcopenshell.file,
reference: ifcopenshell.entity_instance,
attributes: dict[str, Any],
) -> None:
"""Edits the attributes of an IfcDocumentReference
For more information about the attributes and data types of an
IfcDocumentReference, consult the IFC documentation.
For more information about the attributes and data types of an
IfcDocumentReference, consult the IFC documentation.
:param reference: The IfcDocumentReference entity you want to edit
:type reference: ifcopenshell.entity_instance.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:return: None
:rtype: None
:param reference: The IfcDocumentReference entity you want to edit
:type reference: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
document = ifcopenshell.api.run("document.add_information", model)
ifcopenshell.api.run("document.edit_information", model,
information=document,
attributes={"Identification": "A-GA-6100", "Name": "Overall Plan",
"Location": "A-GA-6100 - Overall Plan.pdf"})
reference = ifcopenshell.api.run("document.add_reference", model, information=document)
ifcopenshell.api.run("document.edit_reference", model,
reference=reference, attributes={"Identification": "2.1.15"})
"""
self.file = file
self.settings = {"reference": reference, "attributes": attributes or {}}
document = ifcopenshell.api.run("document.add_information", model)
ifcopenshell.api.run("document.edit_information", model,
information=document,
attributes={"Identification": "A-GA-6100", "Name": "Overall Plan",
"Location": "A-GA-6100 - Overall Plan.pdf"})
reference = ifcopenshell.api.run("document.add_reference", model, information=document)
ifcopenshell.api.run("document.edit_reference", model,
reference=reference, attributes={"Identification": "2.1.15"})
"""
settings = {"reference": reference, "attributes": attributes}
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["reference"], name, value)
for name, value in settings["attributes"].items():
setattr(settings["reference"], name, value)
@@ -22,45 +22,51 @@ import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, information=None):
"""Removes a document information
def remove_information(file: ifcopenshell.file, information: ifcopenshell.entity_instance) -> None:
"""Removes a document information
All references and associations are also removed.
All references and associations are also removed.
:param information: The IfcDocumentInformation to remove
:type information: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param information: The IfcDocumentInformation to remove
:type information: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
# Add a document
document = ifcopenshell.api.run("document.add_information", model)
# ... and remove it!
ifcopenshell.api.run("document.remove_information", model, information=document)
"""
self.file = file
self.settings = {"information": information}
# Add a document
document = ifcopenshell.api.run("document.add_information", model)
# ... and remove it!
ifcopenshell.api.run("document.remove_information", model, information=document)
"""
def execute(self):
for reference in self.settings["information"].HasDocumentReferences or []:
ifcopenshell.api.run("document.remove_reference", self.file, reference=reference)
if file.schema == "IFC2X3":
references = information.DocumentReferences or []
else:
references = information.HasDocumentReferences
for rel in self.settings["information"].IsPointer or []:
for information in rel.RelatedDocuments:
ifcopenshell.api.run("document.remove_information", self.file, information=information)
for reference in references:
ifcopenshell.api.run("document.remove_reference", file, reference=reference)
for rel in self.settings["information"].IsPointedTo or []:
if rel.RelatedDocuments == (self.settings["information"],):
# This relationship is non-rooted
self.file.remove(rel)
for rel in information.IsPointer or []:
for info in rel.RelatedDocuments:
ifcopenshell.api.run("document.remove_information", file, information=info)
for rel in self.settings["information"].DocumentInfoForObjects or []:
history = rel.OwnerHistory
self.file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
self.file.remove(self.settings["information"])
for rel in information.IsPointedTo or []:
if rel.RelatedDocuments == (information,):
# This relationship is non-rooted
file.remove(rel)
if file.schema == "IFC2X3":
rels = [r for r in file.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == information]
else:
rels = information.DocumentInfoForObjects
for rel in rels:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
file.remove(information)
@@ -20,32 +20,33 @@ import ifcopenshell
import ifcopenshell.util.element
class Usecase:
def __init__(self, file: ifcopenshell.file, reference: ifcopenshell.entity_instance):
"""Remove a document reference
def remove_reference(file: ifcopenshell.file, reference: ifcopenshell.entity_instance) -> None:
"""Remove a document reference
All associations with objects are removed.
All associations with objects are removed.
:param reference: The IfcDocumentReference to remove
:type reference: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param reference: The IfcDocumentReference to remove
:type reference: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
document = ifcopenshell.api.run("document.add_information", model)
reference = ifcopenshell.api.run("document.add_reference", model, information=document)
ifcopenshell.api.run("document.remove_reference", model, reference=reference)
"""
self.file = file
self.settings = {"reference": reference}
document = ifcopenshell.api.run("document.add_information", model)
reference = ifcopenshell.api.run("document.add_reference", model, information=document)
ifcopenshell.api.run("document.remove_reference", model, reference=reference)
"""
def execute(self) -> None:
for rel in self.settings["reference"].DocumentRefForObjects or []:
history = rel.OwnerHistory
self.file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
self.file.remove(self.settings["reference"])
if file.schema == "IFC2X3":
rels = [r for r in file.get_inverse(reference) if r.is_a("IfcRelAssociatesDocument")]
else:
rels = reference.DocumentRefForObjects
for rel in rels:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
file.remove(reference)
@@ -21,69 +21,65 @@ import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
def __init__(
self,
file: ifcopenshell.file,
products: list[ifcopenshell.entity_instance],
document: ifcopenshell.entity_instance,
):
"""Unassigns a document and an association to the list of products
def unassign_document(
file: ifcopenshell.file,
products: list[ifcopenshell.entity_instance],
document: ifcopenshell.entity_instance,
) -> None:
"""Unassigns a document and an association to the list of products
:param product: The list of objects that the document reference or information is
related to.
:type product: list[ifcopenshell.entity_instance.entity_instance]
:param document: The IfcDocumentReference (typically) or in rare cases
the IfcDocumentInformation that is associated with the product
:type document: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param product: The list of objects that the document reference or information is
related to.
:type product: list[ifcopenshell.entity_instance]
:param document: The IfcDocumentReference (typically) or in rare cases
the IfcDocumentInformation that is associated with the product
:type document: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
document = ifcopenshell.api.run("document.add_information", model)
ifcopenshell.api.run("document.edit_information", model,
information=document,
attributes={"Identification": "A-GA-6100", "Name": "Overall Plan",
"Location": "A-GA-6100 - Overall Plan.pdf"})
reference = ifcopenshell.api.run("document.add_reference", model, information=document)
document = ifcopenshell.api.run("document.add_information", model)
ifcopenshell.api.run("document.edit_information", model,
information=document,
attributes={"Identification": "A-GA-6100", "Name": "Overall Plan",
"Location": "A-GA-6100 - Overall Plan.pdf"})
reference = ifcopenshell.api.run("document.add_reference", model, information=document)
# Let's imagine storey represents an IfcBuildingStorey for the ground floor
ifcopenshell.api.run("document.assign_document", model, products=[storey], document=reference)
# Let's imagine storey represents an IfcBuildingStorey for the ground floor
ifcopenshell.api.run("document.assign_document", model, products=[storey], document=reference)
# Now let's change our mind and remove the association
ifcopenshell.api.run("document.unassign_document", model, products=[storey], document=reference)
"""
self.file = file
self.settings = {
"products": products,
"document": document,
}
# Now let's change our mind and remove the association
ifcopenshell.api.run("document.unassign_document", model, products=[storey], document=reference)
"""
settings = {
"products": products,
"document": document,
}
def execute(self):
# TODO: do we need to support non-ifcroot elements like we do in classification.add_reference?
# NOTE: reuses code from `library.un assign_reference`
# TODO: do we need to support non-ifcroot elements like we do in classification.add_reference?
# NOTE: reuses code from `library.un assign_reference`
reference_rels: set[ifcopenshell.entity_instance] = set()
products = set(self.settings["products"])
for product in products:
reference_rels.update(product.HasAssociations)
reference_rels: set[ifcopenshell.entity_instance] = set()
products = set(settings["products"])
for product in products:
reference_rels.update(product.HasAssociations)
reference_rels = {
rel
for rel in reference_rels
if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument == self.settings["document"]
}
reference_rels = {
rel
for rel in reference_rels
if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument == settings["document"]
}
for rel in reference_rels:
related_objects = set(rel.RelatedObjects) - products
if related_objects:
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
else:
history = rel.OwnerHistory
self.file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
for rel in reference_rels:
related_objects = set(rel.RelatedObjects) - products
if related_objects:
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", file, **{"element": rel})
else:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
@@ -15,3 +15,22 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Create relationships necessary for smart annotations for drawings
Drawings may be generated from modeled elements and annotations. These
annotations may have relationships which indicate smart data being populated.
"""
from .. import wrap_usecases
from .assign_product import assign_product
from .edit_text_literal import edit_text_literal
from .unassign_product import unassign_product
wrap_usecases(__path__, __name__)
__all__ = [
"assign_product",
"edit_text_literal",
"unassign_product",
]
@@ -18,97 +18,99 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
class Usecase:
def __init__(self, file, relating_product=None, related_object=None):
"""Associates a product and an object, typically for annotation
def assign_product(
file: ifcopenshell.file,
relating_product: ifcopenshell.entity_instance,
related_object: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
"""Associates a product and an object, typically for annotation
Warning: this is an experimental API.
Warning: this is an experimental API.
When you want to draw attention to a feature or characteristic (such as
a dimension, material, or name) or of a product (e.g. wall, slab,
furniture, etc), an annotation object is created. This annotation is
then associated with the product so that it can reference attributes,
properties, and relationships.
When you want to draw attention to a feature or characteristic (such as
a dimension, material, or name) or of a product (e.g. wall, slab,
furniture, etc), an annotation object is created. This annotation is
then associated with the product so that it can reference attributes,
properties, and relationships.
For example, an annotation of a line will be associated with a grid
axis, such that when that grid axis moves, the annotation of that grid
axis (which is typically truncated to the extents of a drawing) will
also move.
For example, an annotation of a line will be associated with a grid
axis, such that when that grid axis moves, the annotation of that grid
axis (which is typically truncated to the extents of a drawing) will
also move.
Another example might be a label of a furniture product, which might
have some text of the name of the furniture to be shown on drawings or
in 3D.
Another example might be a label of a furniture product, which might
have some text of the name of the furniture to be shown on drawings or
in 3D.
:param relating_product: The IfcProduct the object is related to
:type relating_product: ifcopenshell.entity_instance.entity_instance
:param related_object: The object (typically IfcAnnotation) that the
product is related to
:type related_object: ifcopenshell.entity_instance.entity_instance
:return: The created IfcRelAssignsToProduct relationship
:rtype: ifcopenshell.entity_instance.entity_instance
:param relating_product: The IfcProduct the object is related to
:type relating_product: ifcopenshell.entity_instance
:param related_object: The object (typically IfcAnnotation) that the
product is related to
:type related_object: ifcopenshell.entity_instance
:return: The created IfcRelAssignsToProduct relationship
:rtype: ifcopenshell.entity_instance
Example:
Example:
.. code:: python
.. code:: python
furniture = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcFurniture")
annotation = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcAnnotation")
ifcopenshell.api.run("drawing.assign_product", model,
relating_product=furniture, related_object=annotation)
"""
self.file = file
self.settings = {
"relating_product": relating_product,
"related_object": related_object,
}
furniture = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcFurniture")
annotation = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcAnnotation")
ifcopenshell.api.run("drawing.assign_product", model,
relating_product=furniture, related_object=annotation)
"""
settings = {
"relating_product": relating_product,
"related_object": related_object,
}
def execute(self):
is_grid_axis = self.settings["relating_product"].is_a("IfcGridAxis")
is_grid_axis = settings["relating_product"].is_a("IfcGridAxis")
if is_grid_axis:
if self.settings["related_object"].HasAssignments:
for rel in self.settings["related_object"].HasAssignments:
if rel.is_a("IfcRelAssignsToProduct") and rel.Name == self.settings["relating_product"].AxisTag:
return
elif self.settings["related_object"].HasAssignments:
for rel in self.settings["related_object"].HasAssignments:
if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == self.settings["relating_product"]:
if is_grid_axis:
if settings["related_object"].HasAssignments:
for rel in settings["related_object"].HasAssignments:
if rel.is_a("IfcRelAssignsToProduct") and rel.Name == settings["relating_product"].AxisTag:
return
elif settings["related_object"].HasAssignments:
for rel in settings["related_object"].HasAssignments:
if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == settings["relating_product"]:
return
referenced_by = None
referenced_by = None
if is_grid_axis:
axis = self.settings["relating_product"]
grid = None
for attribute in ("PartOfW", "PartOfV", "PartOfU"):
if getattr(axis, attribute, None):
grid = getattr(axis, attribute)[0]
self.settings["relating_product"] = grid
for rel in grid.ReferencedBy:
if rel.Name == axis.AxisTag:
referenced_by = rel
break
elif self.settings["relating_product"].ReferencedBy:
referenced_by = self.settings["relating_product"].ReferencedBy[0]
if is_grid_axis:
axis = settings["relating_product"]
grid = None
for attribute in ("PartOfW", "PartOfV", "PartOfU"):
if getattr(axis, attribute, None):
grid = getattr(axis, attribute)[0]
settings["relating_product"] = grid
for rel in grid.ReferencedBy:
if rel.Name == axis.AxisTag:
referenced_by = rel
break
elif settings["relating_product"].ReferencedBy:
referenced_by = settings["relating_product"].ReferencedBy[0]
if referenced_by:
related_objects = list(referenced_by.RelatedObjects)
related_objects.append(self.settings["related_object"])
referenced_by.RelatedObjects = related_objects
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": referenced_by})
else:
referenced_by = self.file.create_entity(
"IfcRelAssignsToProduct",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
"RelatedObjects": [self.settings["related_object"]],
"RelatingProduct": self.settings["relating_product"],
}
)
if referenced_by:
related_objects = list(referenced_by.RelatedObjects)
related_objects.append(settings["related_object"])
referenced_by.RelatedObjects = related_objects
ifcopenshell.api.run("owner.update_owner_history", file, **{"element": referenced_by})
else:
referenced_by = file.create_entity(
"IfcRelAssignsToProduct",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", file),
"RelatedObjects": [settings["related_object"]],
"RelatingProduct": settings["relating_product"],
},
)
if is_grid_axis:
referenced_by.Name = axis.AxisTag
return referenced_by
if is_grid_axis:
referenced_by.Name = axis.AxisTag
return referenced_by
@@ -15,33 +15,34 @@
#
# 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
from typing import Any
class Usecase:
def __init__(self, file, text_literal=None, attributes=None):
"""Edits the attributes of an IfcTextLiteral
def edit_text_literal(
file: ifcopenshell.file, text_literal: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcTextLiteral
For more information about the attributes and data types of an
IfcTextLiteral, consult the IFC documentation.
For more information about the attributes and data types of an
IfcTextLiteral, consult the IFC documentation.
:param reference: The IfcTextLiteral entity you want to edit
:type reference: ifcopenshell.entity_instance.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:return: None
:rtype: None
:param reference: The IfcTextLiteral entity you want to edit
:type reference: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
text = model.createIfcTextLiteral()
ifcopenshell.api.run("drawing.edit_text_literal", model,
text_literal=text, attributes={"Literal": "MY ANNOTATION"})
"""
self.file = file
self.settings = {"text_literal": text_literal, "attributes": attributes or {}}
text = model.createIfcTextLiteral()
ifcopenshell.api.run("drawing.edit_text_literal", model,
text_literal=text, attributes={"Literal": "MY ANNOTATION"})
"""
settings = {"text_literal": text_literal, "attributes": attributes or {}}
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["text_literal"], name, value)
for name, value in settings["attributes"].items():
setattr(settings["text_literal"], name, value)
@@ -21,54 +21,54 @@ import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, relating_product=None, related_object=None):
"""Unassigns a product and an object (typically an annotation)
def unassign_product(
file: ifcopenshell.file,
relating_product: ifcopenshell.entity_instance,
related_object: ifcopenshell.entity_instance,
) -> None:
"""Unassigns a product and an object (typically an annotation)
Smart annotation objects can be associated with products so that they
can annotate attributes and properties. This function lets you remove
the association, so that you may change the assocation with another
object later or leave the annotation as a "dumb" annotation.
Smart annotation objects can be associated with products so that they
can annotate attributes and properties. This function lets you remove
the association, so that you may change the assocation with another
object later or leave the annotation as a "dumb" annotation.
:param relating_product: The IfcProduct the object is related to
:type relating_product: ifcopenshell.entity_instance.entity_instance
:param related_object: The object (typically IfcAnnotation) that the
product is related to
:type related_object: ifcopenshell.entity_instance.entity_instance
:return: The created IfcRelAssignsToProduct relationship
:rtype: ifcopenshell.entity_instance.entity_instance
:param relating_product: The IfcProduct the object is related to
:type relating_product: ifcopenshell.entity_instance
:param related_object: The object (typically IfcAnnotation) that the
product is related to
:type related_object: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
Example:
.. code:: python
.. code:: python
furniture = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcFurniture")
annotation = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcAnnotation")
ifcopenshell.api.run("drawing.assign_product", model,
relating_product=furniture, related_object=annotation)
furniture = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcFurniture")
annotation = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcAnnotation")
ifcopenshell.api.run("drawing.assign_product", model,
relating_product=furniture, related_object=annotation)
# Let's change our mind and remove the relationship
ifcopenshell.api.run("drawing.unassign_product", model,
relating_product=furniture, related_object=annotation)
"""
self.file = file
self.settings = {
"relating_product": relating_product,
"related_object": related_object,
}
# Let's change our mind and remove the relationship
ifcopenshell.api.run("drawing.unassign_product", model,
relating_product=furniture, related_object=annotation)
"""
settings = {
"relating_product": relating_product,
"related_object": related_object,
}
def execute(self):
for rel in self.settings["related_object"].HasAssignments or []:
if not rel.is_a("IfcRelAssignsToProduct") or rel.RelatingProduct != self.settings["relating_product"]:
continue
if len(rel.RelatedObjects) == 1:
history = rel.OwnerHistory
self.file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
return
related_objects = list(rel.RelatedObjects)
related_objects.remove(self.settings["related_object"])
rel.RelatedObjects = related_objects
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
return rel
for rel in settings["related_object"].HasAssignments or []:
if not rel.is_a("IfcRelAssignsToProduct") or rel.RelatingProduct != settings["relating_product"]:
continue
if len(rel.RelatedObjects) == 1:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
return
related_objects = list(rel.RelatedObjects)
related_objects.remove(settings["related_object"])
rel.RelatedObjects = related_objects
ifcopenshell.api.run("owner.update_owner_history", file, **{"element": rel})
@@ -15,3 +15,74 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Create geometric representations and assign them to elements
These functions support both the creation of arbitrary geometry as well as
geometry that follows parametric rules (e.g. layered geometry or profiled
geometry extrusions).
"""
from .. import wrap_usecases
from .add_axis_representation import add_axis_representation
from .add_boolean import add_boolean
try:
from .add_door_representation import add_door_representation
except ModuleNotFoundError as e:
print(f"Note: API not available due to missing dependencies: geometry.add_door_representation - {e}")
from .add_footprint_representation import add_footprint_representation
from .add_mesh_representation import add_mesh_representation
from .add_profile_representation import add_profile_representation
try:
from .add_railing_representation import add_railing_representation
except ModuleNotFoundError as e:
print(f"Note: API not available due to missing dependencies: geometry.add_railing_representation - {e}")
try:
from .add_representation import add_representation
except ModuleNotFoundError as e:
print(f"Note: API not available due to missing dependencies: geometry.add_representation - {e}")
from .add_slab_representation import add_slab_representation
from .add_wall_representation import add_wall_representation
try:
from .add_window_representation import add_window_representation
except ModuleNotFoundError as e:
print(f"Note: API not available due to missing dependencies: geometry.add_window_representation - {e}")
from .assign_representation import assign_representation
from .connect_element import connect_element
from .connect_path import connect_path
from .create_2pt_wall import create_2pt_wall
from .disconnect_element import disconnect_element
from .disconnect_path import disconnect_path
from .edit_object_placement import edit_object_placement
from .map_representation import map_representation
from .remove_boolean import remove_boolean
from .remove_representation import remove_representation
from .unassign_representation import unassign_representation
wrap_usecases(__path__, __name__)
__all__ = [
"add_axis_representation",
"add_boolean",
"add_door_representation",
"add_footprint_representation",
"add_mesh_representation",
"add_profile_representation",
"add_railing_representation",
"add_representation",
"add_slab_representation",
"add_wall_representation",
"add_window_representation",
"assign_representation",
"connect_element",
"connect_path",
"create_2pt_wall",
"disconnect_element",
"disconnect_path",
"edit_object_placement",
"map_representation",
"remove_boolean",
"remove_representation",
"unassign_representation",
]
@@ -17,63 +17,71 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.unit
from typing import Union
COORD = Union[tuple[float, float], tuple[float, float, float]]
def add_axis_representation(
file: ifcopenshell.file, context: ifcopenshell.entity_instance, axis: tuple[COORD, COORD]
) -> ifcopenshell.entity_instance:
"""Adds a new axis representation
Certain objects are typically "axis-based", such as walls, beams,
and columns. This means you can represent them abstractly by simply
drawing a single line either in 2D (such as for walls) or 3D (for beams
and columns). Humans can understand this axis-based representation as
being a simplification of a layered extrusion or a profile that is being
extruded along that axis and joined to other elements.
Using an axis-based representation makes it easy for users and computers
to analyse connectivity and spatial relationships, as well as makes it
easy to parametrically edit these objects by simply stretching the start
or end of the axis.
For now, only simple straight line axes are supported, represented by a
start and end coordinate. The order is important. For walls, the start
must be at the minimum local X ordinate, and the end at the maximum
local X ordinate. For beams and columns, the start is at the minimum
local Z ordinate, and the end of the maximum local Z ordinate. The first
coordinate is the "start" and the second coordinate is the "end". This
stat and end is then used to determine any parametric junctions with
other elements.
Using an axis-representation is optional, but highly recommended for
"standard" representations of walls, beams, columns, and other
structural members. A rule of thumb is that if you can draw it as a line
on paper, you can probably represent it using an axis.
:param context: The IfcGeometricRepresentationContext that the
representation is part of. This must be either a
Model/Axis/GRAPH_VIEW (3D) or Plan/Axis/GRAPH_VIEW (2D).
:type context: ifcopenshell.entity_instance
:param axis: The axis, as a list of two coordinates, the coordinates
being either a list of 2 or 3 float coordinates depending on whether
the axis is 2D or 3D.
:type axis: list[list[float]]
:return: The newly created IfcShapeRepresentation entity
:rtype: ifcopenshell.entity_instance
Example:
.. code:: python
context = ifcopenshell.util.representation.get_context(model, "Plan", "Axis", "GRAPH_VIEW")
axis = ifcopenshell.api.run("geometry.add_axis_representation", model,
context=context, axis=[(0.0, 0.0), (1.0, 0.0)])
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"context": context,
"axis": axis or [],
}
return usecase.execute()
class Usecase:
def __init__(self, file, context=None, axis=None):
"""Adds a new axis representation
Certain objects are typically "axis-based", such as walls, beams,
and columns. This means you can represent them abstractly by simply
drawing a single line either in 2D (such as for walls) or 3D (for beams
and columns). Humans can understand this axis-based representation as
being a simplification of a layered extrusion or a profile that is being
extruded along that axis and joined to other elements.
Using an axis-based representation makes it easy for users and computers
to analyse connectivity and spatial relationships, as well as makes it
easy to parametrically edit these objects by simply stretching the start
or end of the axis.
For now, only simple straight line axes are supported, represented by a
start and end coordinate. The order is important. For walls, the start
must be at the minimum local X ordinate, and the end at the maximum
local X ordinate. For beams and columns, the start is at the minimum
local Z ordinate, and the end of the maximum local Z ordinate. The first
coordinate is the "start" and the second coordinate is the "end". This
stat and end is then used to determine any parametric junctions with
other elements.
Using an axis-representation is optional, but highly recommended for
"standard" representations of walls, beams, columns, and other
structural members. A rule of thumb is that if you can draw it as a line
on paper, you can probably represent it using an axis.
:param context: The IfcGeometricRepresentationContext that the
representation is part of. This must be either a
Model/Axis/GRAPH_VIEW (3D) or Plan/Axis/GRAPH_VIEW (2D).
:type context: ifcopenshell.entity_instance.entity_instance
:param axis: The axis, as a list of two coordinates, the coordinates
being either a list of 2 or 3 float coordinates depending on whether
the axis is 2D or 3D.
:type axis: list[list[float]]
:return: The newly created IfcShapeRepresentation entity
:rtype: ifcopenshell.entity_instance.entity_instance
Example:
.. code:: python
context = ifcopenshell.util.representation.get_context(model, "Plan", "Axis", "GRAPH_VIEW")
axis = ifcopenshell.api.run("geometry.add_axis_representation", model,
context=context, axis=[(0.0, 0.0), (1.0, 0.0)])
"""
self.file = file
self.settings = {
"context": context,
"axis": axis or [],
}
def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
is_2d = len(self.settings["axis"][0]) == 2
@@ -82,9 +90,13 @@ class Usecase:
curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points])
else:
if is_2d:
curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList2D(points), None, False)
curve = self.file.createIfcIndexedPolyCurve(
self.file.createIfcCartesianPointList2D(points), None, False
)
else:
curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList3D(points), None, False)
curve = self.file.createIfcIndexedPolyCurve(
self.file.createIfcCartesianPointList3D(points), None, False
)
return self.file.createIfcShapeRepresentation(
self.settings["context"],
self.settings["context"].ContextIdentifier,
@@ -16,28 +16,55 @@
# 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 __future__ import annotations
import ifcopenshell.util.unit
import numpy as np
import numpy.typing as npt
from typing import Optional, TYPE_CHECKING, Literal
if TYPE_CHECKING:
import bpy.types
NPArrayOfFloats = npt.NDArray[np.float64]
def add_boolean(
file: ifcopenshell.file,
representation: ifcopenshell.entity_instance,
# A matrix to define a clipping Ifchalfspacesolid.
# The XY plane is the clipping boundary and +Z is removed.
operator: str = "DIFFERENCE",
# IfcHalfSpaceSolid, Mesh
type: Literal["IfcHalfSpaceSolid", "Mesh"] = "IfcHalfSpaceSolid",
matrix: Optional[NPArrayOfFloats] = None,
# A Blender OBJ to define the voided OBJ for a "Mesh" type
blender_obj: Optional[bpy.types.Object] = None,
# A Blender OBJ to define the void OBJ for a "Mesh" type
blender_void: Optional[bpy.types.Object] = None,
should_force_faceted_brep: bool = False,
should_force_triangulation: bool = False,
) -> list[ifcopenshell.entity_instance]:
"""For `type` values:
- "IfcHalfSpaceSolid" - `matrix` is not optional.
- "Mesh" - `blender_obj` and `blender_void` are not optional
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"representation": representation,
"operator": operator,
"type": type,
"matrix": matrix,
"blender_obj": blender_obj,
"blender_void": blender_void,
"should_force_faceted_brep": should_force_faceted_brep,
"should_force_triangulation": should_force_triangulation,
}
return usecase.execute()
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"representation": None,
"operator": "DIFFERENCE",
# IfcHalfSpaceSolid, Mesh
"type": "IfcHalfSpaceSolid",
# The XY plane is the clipping boundary and +Z is removed.
"matrix": None, # A matrix to define a clipping Ifchalfspacesolid.
"blender_obj": None, # A Blender OBJ to define the voided OBJ for a "Mesh" type
"blender_void": None, # A Blender OBJ to define the void OBJ for a "Mesh" type
"should_force_faceted_brep": False,
"should_force_triangulation": False,
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
if self.settings["type"] == "IfcHalfSpaceSolid":
@@ -16,13 +16,15 @@
# 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 __future__ import annotations
import collections.abc
import ifcopenshell.util.unit
from ifcopenshell.util.shape_builder import ShapeBuilder, V
from ifcopenshell.api.geometry.add_window_representation import create_ifc_window
from mathutils import Vector
from math import cos, radians
import collections
from typing import Any, Optional, Literal, Union
import dataclasses
SUPPORTED_DOOR_TYPES = (
@@ -38,9 +40,14 @@ SUPPORTED_DOOR_TYPES = (
)
def mm(x: float) -> float:
"""mm to meters shortcut for readability"""
return x / 1000
def create_ifc_door_lining(
builder: ShapeBuilder, size: Vector, thickness: list, position: Vector = V(0, 0, 0).freeze()
):
) -> ifcopenshell.entity_instance:
"""`thickness` of the profile is defined as list in the following order: `(SIDE, TOP)`
`thickness` can be also defined just as 1 float value.
@@ -63,91 +70,222 @@ def create_ifc_door_lining(
points = [p.xz for p in points]
door_lining = builder.polyline(points, closed=True)
door_lining = builder.extrude(
door_lining,
size.y,
**builder.extrude_kwargs("Y")
)
door_lining = builder.extrude(door_lining, size.y, **builder.extrude_kwargs("Y"))
builder.translate(door_lining, position)
return door_lining
def create_ifc_box(builder: ShapeBuilder, size: Vector, position: Vector = V(0, 0, 0).freeze()):
def create_ifc_box(
builder: ShapeBuilder, size: Vector, position: Vector = V(0, 0, 0).freeze()
) -> ifcopenshell.entity_instance:
rect = builder.rectangle(size.xy)
box = builder.extrude(rect, size.z, position=position, extrusion_vector=V(0, 0, 1))
return box
class Usecase:
def __init__(self, file, **settings):
"""units in settings expected to be in ifc project units"""
self.file = file
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoor.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorTypeOperationEnum.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorLiningProperties.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorPanelProperties.htm
self.settings = {"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(self.file)}
self.settings.update(
{
"context": None, # IfcGeometricRepresentationContext
"overall_height": self.convert_si_to_unit(2.0),
"overall_width": self.convert_si_to_unit(0.9),
# DOUBLE_DOOR_DOUBLE_SWING, DOUBLE_DOOR_FOLDING, DOUBLE_DOOR_LIFTING_VERTICAL,
# DOUBLE_DOOR_SINGLE_SWING, DOUBLE_DOOR_SINGLE_SWING_OPPOSITE_LEFT,
# DOUBLE_DOOR_SINGLE_SWING_OPPOSITE_RIGHT, DOUBLE_DOOR_SLIDING,
# DOUBLE_SWING_LEFT, DOUBLE_SWING_RIGHT, FOLDING_TO_LEFT,
# FOLDING_TO_RIGHT, LIFTING_HORIZONTAL, LIFTING_VERTICAL_LEFT,
# LIFTING_VERTICAL_RIGHT, REVOLVING, REVOLVING_VERTICAL,
# ROLLINGUP, SINGLE_SWING_LEFT, SINGLE_SWING_RIGHT, SLIDING_TO_LEFT,
# SLIDING_TO_RIGHT, SWING_FIXED_LEFT, SWING_FIXED_RIGHT
"operation_type": "SINGLE_SWING_LEFT", # door type
"lining_properties": {
"LiningDepth": self.convert_si_to_unit(0.050),
"LiningThickness": self.convert_si_to_unit(0.050),
# offset from the outer side of the wall (by Y-axis)
"LiningOffset": self.convert_si_to_unit(0.0),
# offset from the wall
"LiningToPanelOffsetX": self.convert_si_to_unit(0.025),
# offset from the X-axis (unlike windows)
"LiningToPanelOffsetY": self.convert_si_to_unit(0.025),
# transom - vertical distance between door and window panels
"TransomThickness": self.convert_si_to_unit(0.000),
# TransomOffset - distance from the bottom door opening
# to the beginning of the transom
# unlike windows TransomOffset which goes to the center of the transom
"TransomOffset": self.convert_si_to_unit(1.525),
"ShapeAspectStyle": None, # DEPRECATED
# Casing cover wall faces around the opening
# on the left, right and upper sides
# Casing should be either on both sides of the wall or no casing
# If `LiningOffset` is present then therefore casing is not possible on outer wall
# therefore there will be no casing on inner wall either
"CasingDepth": self.convert_si_to_unit(0.005),
"CasingThickness": self.convert_si_to_unit(0.075), # by Z-axis
# Threshold covers the bottom side of the opening
"ThresholdDepth": self.convert_si_to_unit(0.1),
"ThresholdThickness": self.convert_si_to_unit(0.025), # by Z-axis
# offset by Y-axis
"ThresholdOffset": self.convert_si_to_unit(0.000),
},
"panel_properties": {
"PanelDepth": self.convert_si_to_unit(0.035), # by Y
"PanelWidth": 1.0, # as ratio to the clear door opening
"FrameDepth": self.convert_si_to_unit(0.035), # by Y
"FrameThickness": self.convert_si_to_unit(0.035), # by X
# LEFT, MIDDLE, RIGHT, NOTDEFINED
"PanelPosition": ..., # NEVER USED
# defines the basic ways to describe how door panels operate
# basically how it opens
"PanelOperation": None, # NEVER USED
"ShapeAspectStyle": None, # DEPRECATED
},
}
)
for key, value in settings.items():
self.settings[key] = value
# we use dataclass as we need default values for arguments
# it's okay to use slots since we don't need dynamic attributes
@dataclasses.dataclass(slots=True)
class DoorLiningProperties:
LiningDepth: Optional[float] = None
"""Optional, defaults to 50mm."""
LiningThickness: Optional[float] = None
"""Optional, defaults to 50mm."""
LiningOffset: Optional[float] = None
"""Offset from the outer side of the wall (by Y-axis). Optional, defaults to 0.0."""
LiningToPanelOffsetX: Optional[float] = None
"""Offset from the wall. Optional, defaults to 25mm."""
LiningToPanelOffsetY: Optional[float] = None
"""Offset from the X-axis (unlike windows). Optional, defaults to 25mm."""
TransomThickness: Optional[float] = None
"""Vertical distance between door and window panels. Optional, defaults to 0.0."""
TransomOffset: Optional[float] = None
"""Distance from the bottom door opening
to the beginning of the transom
unlike windows TransomOffset which goes to the center of the transom.
Optional, defaults 1.525m."""
ShapeAspectStyle: None = None
"""Optional. Deprecated argument."""
CasingDepth: Optional[float] = None
"""Casing cover wall faces around the opening
on the left, right and upper sides
Casing should be either on both sides of the wall or no casing
If `LiningOffset` is present then therefore casing is not possible on outer wall
therefore there will be no casing on inner wall either. Optional, defaults to 5mm."""
CasingThickness: Optional[float] = None
"""Casing thickness by Z-axis. Optional, defaults to 75mm."""
ThresholdDepth: Optional[float] = None
"""Threshold covers the bottom side of the opening. Optional, defaults to 100mm."""
ThresholdThickness: Optional[float] = None
"""Theshold thickness by Z-axis. Optional, defaults to 25mm."""
ThresholdOffset: Optional[float] = None
"""Threshold offset by Y-axis. Optional, defaults to 0.0."""
def initialize_properties(self, unit_scale: float) -> None:
# in meters
# fmt: off
default_values: dict[str, float] = dict(
LiningDepth = mm(50),
LiningThickness = mm(50),
LiningOffset = 0.0,
LiningToPanelOffsetX = mm(25),
LiningToPanelOffsetY = mm(25),
TransomThickness = 0.0,
TransomOffset = mm(1525),
CasingDepth = mm(5),
CasingThickness = mm(75),
ThresholdDepth = mm(100),
ThresholdThickness = mm(25),
ThresholdOffset = 0.0,
)
# fmt: on
si_conversion = 1 / unit_scale
for attr, default_value in default_values.items():
if getattr(self, attr) is not None:
continue
setattr(self, attr, default_value * si_conversion)
@dataclasses.dataclass(slots=True)
class DoorPanelProperties:
PanelDepth: Optional[float] = None
"""Frame thickness by Y axis. Optional, defaults to 35 mm."""
PanelWidth: float = 1.0
"""Ratio to the clear door opening. Optional, defaults to 1.0."""
FrameDepth: Optional[float] = None
"""Frame thickness by Y axis. Optional, defaults to 35 mm."""
FrameThickness: Optional[float] = None
"""Frame thickness by X axis. Optional, defaults to 35 mm."""
PanelPosition: None = None
"""Optional, value is never used"""
PanelOperation: None = None
"""Optional, value is never used.
Defines the basic ways to describe how door panels operate."""
ShapeAspectStyle: None = None
"""Optional. Deprecated argument."""
def initialize_properties(self, unit_scale: float) -> None:
# in meters
# fmt: off
default_values: dict[str, float] = dict(
PanelDepth = mm(35),
FrameDepth = mm(35),
FrameThickness = mm(35),
)
# fmt: on
si_conversion = 1 / unit_scale
for attr, default_value in default_values.items():
if getattr(self, attr) is not None:
continue
setattr(self, attr, default_value * si_conversion)
def add_door_representation(
file: ifcopenshell.file,
*, # keywords only as this API implementation is probably not final
context: ifcopenshell.entity_instance,
overall_height: Optional[float] = None,
overall_width: Optional[float] = None,
# door type
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorTypeOperationEnum.htm
operation_type: Literal[
"SINGLE_SWING_LEFT",
"SINGLE_SWING_RIGHT",
"DOUBLE_SWING_RIGHT",
"DOUBLE_SWING_LEFT",
"DOUBLE_DOOR_SINGLE_SWING",
"DOUBLE_DOOR_DOUBLE_SWING",
"SLIDING_TO_LEFT",
"SLIDING_TO_RIGHT",
"DOUBLE_DOOR_SLIDING",
] = "SINGLE_SWING_LEFT",
lining_properties: Optional[Union[DoorLiningProperties, dict[str, Any]]] = None,
panel_properties: Optional[Union[DoorPanelProperties, dict[str, Any]]] = None,
unit_scale: Optional[float] = None,
) -> ifcopenshell.entity_instance:
"""units in usecase_settings expected to be in ifc project units
:param context: IfcGeometricRepresentationContext for the representation.
:type context: ifcopenshell.entity_instance
:param overall_height: Overall door height. Defaults to 2m.
:type overall_height: float, optional
:param overall_width: Overall door width. Defaults to 0.9m.
:type overall_width: float, optional
:param operation_type: Type of the door. Defaults to SINGLE_SWING_LEFT.
:type operation_type: str, optional
:param lining_properties: DoorLiningProperties or a dictionary to create one.
See DoorLiningProperties description for details.
:type lining_properties: Union[DoorLiningProperties, dict[str, Any]]]
:param panel_properties: DoorPanelProperties or a dictionary to create one.
See DoorPanelProperties description for details.
:type panel_properties: Union[DoorPanelProperties, dict[str, Any]]]
:param unit_scale: The unit scale as calculated by
ifcopenshell.util.unit.calculate_unit_scale. If not provided, it
will be automatically calculated for you.
:type unit_scale: float, optional
:return: IfcShapeRepresentation for a door.
:rtype: ifcopenshell.entity_instance
"""
usecase = Usecase()
usecase.file = file
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoor.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorLiningProperties.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorPanelProperties.htm
# define unit_scale first as it's going to be used setting default arguments
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) if unit_scale is None else unit_scale
settings: dict[str, Any] = {"unit_scale": unit_scale}
if lining_properties is None:
lining_properties = DoorLiningProperties()
elif not isinstance(lining_properties, DoorLiningProperties):
lining_properties = DoorLiningProperties(**lining_properties)
lining_properties.initialize_properties(unit_scale)
lining_properties = dataclasses.asdict(lining_properties)
if panel_properties is None:
panel_properties = DoorPanelProperties()
elif not isinstance(panel_properties, DoorPanelProperties):
panel_properties = DoorPanelProperties(**panel_properties)
panel_properties.initialize_properties(unit_scale)
panel_properties = dataclasses.asdict(panel_properties)
settings.update(
{
"context": context,
"overall_height": overall_height if overall_height is not None else usecase.convert_si_to_unit(2.0),
"overall_width": overall_width if overall_width is not None else usecase.convert_si_to_unit(0.9),
"operation_type": operation_type,
"lining_properties": lining_properties,
"panel_properties": panel_properties,
}
)
usecase.settings = settings
return usecase.execute()
class Usecase:
def execute(self):
builder = ShapeBuilder(self.file)
overall_height = self.settings["overall_height"]
@@ -19,20 +19,21 @@
import ifcopenshell.util.unit
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"context": None, # IfcGeometricRepresentationContext
"curves": [], # A list of IFC curves to include in the curve set
}
for key, value in settings.items():
self.settings[key] = value
def add_footprint_representation(
file,
# IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
# A list of IFC curves to include in the curve set
curves: list[ifcopenshell.entity_instance],
) -> ifcopenshell.entity_instance:
settings = {
"context": context,
"curves": curves,
}
def execute(self):
return self.file.createIfcShapeRepresentation(
self.settings["context"],
self.settings["context"].ContextIdentifier,
"GeometricCurveSet",
[self.file.createIfcGeometricCurveSet(self.settings["curves"])],
)
return file.createIfcShapeRepresentation(
settings["context"],
settings["context"].ContextIdentifier,
"GeometricCurveSet",
[file.createIfcGeometricCurveSet(settings["curves"])],
)
@@ -17,27 +17,47 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.unit
from typing import Optional
COORD_3D = tuple[float, float, float]
def add_mesh_representation(
file: ifcopenshell.file,
# IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
# Vertices, edges, and faces are given in the form of: [item1, item2, item3, ...]
# A list of coordinates
# ... where itemN = [(0., 0., 0.), (1., 1., 1.), (x, y, z), ...]
vertices: list[COORD_3D],
# A list of edges, represented by vertex index pairs
# ... where itemN = [(0, 1), (1, 2), (v1, v2), ...]
edges: list[tuple[int, int]],
# A list of polygons, represented by vertex indices
# ... where itemN = [(0, 1, 2), (5, 4, 2, 3), (v1, v2, v3, ... vN), ...]
faces: list[list[int]],
# Optionally apply a vector offset to all coordinates
cooridnate_offset: Optional[COORD_3D] = None,
# A scale factor to apply for all vectors in case the unit is different
unit_scale: Optional[float] = None,
# Force using IfcFacetedBreps instead of IfcPolygonalFaceSets
force_faceted_brep: bool = False,
) -> ifcopenshell.entity_instance:
usecase = Usecase()
usecase.file = file
usecase.settings = {
"context": context,
"vertices": vertices,
"edges": edges,
"faces": faces,
"coordinate_offset": cooridnate_offset,
"unit_scale": unit_scale,
"force_faceted_brep": force_faceted_brep,
}
return usecase.execute()
class Usecase:
def __init__(self, file: ifcopenshell.file, **settings):
self.file = file
self.settings = {
"context": None, # IfcGeometricRepresentationContext
# Vertices, edges, and faces are given in the form of: [item1, item2, item3, ...]
# ... where itemN = [(0., 0., 0.), (1., 1., 1.), (x, y, z), ...]
"vertices": None, # A list of coordinates
# ... where itemN = [(0, 1), (1, 2), (v1, v2), ...]
"edges": None, # A list of edges, represented by vertex index pairs
# ... where itemN = [(0, 1, 2), (5, 4, 2, 3), (v1, v2, v3, ... vN), ...]
"faces": None, # A list of polygons, represented by vertex indices
"coordinate_offset": None, # Optionally apply a vector offset to all coordinates
"unit_scale": None, # A scale factor to apply for all vectors in case the unit is different
"force_faceted_brep": False, # Force using IfcFacetedBreps instead of IfcPolygonalFaceSets
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if self.settings["unit_scale"] is None:
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -19,24 +19,39 @@
import ifcopenshell.geom
import ifcopenshell.util.unit
from ifcopenshell.util.data import Clipping
from typing import Any, Union, Optional, Literal
VECTOR_3D = tuple[float, float, float]
def add_profile_representation(
file: ifcopenshell.file,
# IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
profile: ifcopenshell.entity_instance,
# in meters
depth: float = 1.0,
cardinal_point: Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] = 5,
# A list of planes that define clipping half space solids
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None,
placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]] = (None, None),
) -> None:
usecase = Usecase()
usecase.file = file
usecase.settings = {
"context": context,
"profile": profile,
"depth": depth,
"cardinal_point": cardinal_point,
"clippings": clippings if clippings is not None else [],
"placement_zx_axes": placement_zx_axes,
}
return usecase.execute()
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"context": None, # IfcGeometricRepresentationContext
"profile": None,
"depth": 1.0,
"cardinal_point": 5,
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
"clippings": [], # A list of planes that define clipping half space solids
"placement_zx_axes": (None, None),
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
self.settings["clippings"] = [Clipping.parse(c) for c in self.settings["clippings"]]
@@ -22,48 +22,105 @@ from itertools import chain
from mathutils import Vector, Matrix
import collections
import mathutils
from pprint import pprint
from math import pi, cos, sin, tan, radians
from typing import Literal, Optional, Any
def mm(x):
def mm(x: float) -> float:
"""mm to meters shortcut for readability"""
return x / 1000
def add_railing_representation(
file: ifcopenshell.file,
*, # keywords only as this API implementation is probably not final
# IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
railing_type: Literal["WALL_MOUNTED_HANDRAIL"] = "WALL_MOUNTED_HANDRAIL",
railing_path: list[Vector],
use_manual_supports: bool = False,
support_spacing: Optional[float] = None,
railing_diameter: Optional[float] = None,
clear_width: Optional[float] = None,
terminal_type: Literal[
"180",
"TO_END_POST",
"TO_WALL",
"TO_FLOOR",
"TO_END_POST_AND_FLOOR",
] = "180",
height: Optional[float] = None,
looped_path: bool = False,
unit_scale: Optional[float] = None,
) -> ifcopenshell.entity_instance:
"""
Units are expected to be in IFC project units.
:param context: IfcGeometricRepresentationContext for the representation.
:type context: ifcopenshell.entity_instance
:param railing_type: Type of the railing. Defaults to "WALL_MOUNTED_HANDRAIL".
:type railing_type: Literal["WALL_MOUNTED_HANDRAIL"], optional
:param railing_path: A list of points coordinates for the railing path,
coordinates are expected to be at the top of the railing, not at the center.
If not provided, default path [(0, 0, 1), (1, 0, 1), (2, 0, 1)] (in meters) will be used
:type railing_path: list[Vector], optional.
:param use_manual_supports: If enabled, supports are added on every vertex on the edges of the railing path.
If disabled, supports are added automatically based on the support spacing. Default to False.
:type use_manual_supports: bool, optional
:param support_spacing: Distance between supports if automatic supports are used. Defaults to 1m.
:type support_spacing: float, optional
:param railing_diameter: Railing diameter. Defaults to 50mm.
:type railing_diameter: float, optional
:param clear_width: Clear width between the railing and the wall. Defaults to 40mm.
:type clear_width: float, optional
:param terminal_type: type of the cap. Defaults to "180".
:type terminal_type: Literal["180","TO_END_POST","TO_WALL","TO_FLOOR","TO_END_POST_AND_FLOOR"], optional
:param height: defaults to 1m
:type height: float, optional
:param looped_path: Whether to end the railing on the first point of `railing_path`. Defaults to False.
:type looped_path: bool, optional
:param unit_scale: The unit scale as calculated by
ifcopenshell.util.unit.calculate_unit_scale. If not provided, it
will be automatically calculated for you.
:type unit_scale: float, optional
:return: IfcShapeRepresentation for a railing.
:rtype: ifcopenshell.entity_instance
"""
usecase = Usecase()
usecase.file = file
# define unit_scale first as it's going to be used setting default arguments
settings: dict[str, Any] = {
"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(file) if unit_scale is None else unit_scale,
}
settings.update(
{
"context": context,
"railing_type": railing_path,
"railing_path": (
railing_path
if railing_path is not None
else usecase.path_si_to_units([V(0, 0, 1), V(1, 0, 1), V(2, 0, 1)])
),
"use_manual_supports": use_manual_supports,
"support_spacing": support_spacing if support_spacing is not None else usecase.convert_si_to_unit(mm(1000)),
"railing_diameter": (
railing_diameter if railing_diameter is not None else usecase.convert_si_to_unit(mm(50))
),
"clear_width": clear_width if clear_width is not None else usecase.convert_si_to_unit(mm(40)),
"terminal_type": terminal_type,
"height": height if height is not None else usecase.convert_si_to_unit(mm(1000)),
"looped_path": looped_path,
}
)
usecase.settings = settings
if railing_type != "WALL_MOUNTED_HANDRAIL":
raise Exception('Only "WALL_MOUNTED_HANDRAIL" railing_type is supported at the moment.')
return usecase.execute()
class Usecase:
def __init__(self, file, **settings):
"""
units in settings expected to be in ifc project units
`railing_path` is a list of point coordinates for the railing path,
coordinates are expected to be at the top of the railing, not at the center
`railing_path` is expected to be a list of Vector objects
"""
self.file = file
self.settings = {"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(self.file)}
self.settings.update(
{
"context": None, # IfcGeometricRepresentationContext
"railing_type": "WALL_MOUNTED_HANDRAIL",
"railing_path": self.path_si_to_units([V(0, 0, 1), V(1, 0, 1), V(2, 0, 1)]),
"use_manual_supports": False,
"support_spacing": self.convert_si_to_unit(mm(1000)),
"railing_diameter": self.convert_si_to_unit(mm(50)),
"clear_width": self.convert_si_to_unit(mm(40)),
"terminal_type": "180",
"height": self.convert_si_to_unit(mm(1000)),
"looped_path": False,
}
)
for key, value in settings.items():
self.settings[key] = value
if self.settings["railing_type"] != "WALL_MOUNTED_HANDRAIL":
raise Exception('Only "WALL_MOUNTED_HANDRAIL" railing_type is supported at the moment.')
def execute(self):
arc_points = []
items_3d = []
@@ -15,48 +15,91 @@
#
# 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 bpy
from __future__ import annotations
import bpy.types
import math
import bmesh
import ifcopenshell.util.unit
from mathutils import Vector, Matrix
from blenderbim.bim.module.geometry.helper import Helper
from typing import Union, Optional, Literal, Any, TYPE_CHECKING
if TYPE_CHECKING:
from blenderbim.bim.module.geometry.helper import Helper
Z_AXIS = Vector((0, 0, 1))
X_AXIS = Vector((1, 0, 0))
EPSILON = 1e-6
def add_representation(
file: ifcopenshell.file,
*, # keywords only as this API implementation is probably not final
# IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
# This is (currently) a Blender object, hence this depends on Blender now
blender_object: bpy.types.Object,
# This is (currently) a Blender data object, hence this depends on Blender now
geometry: Union[bpy.types.Mesh, bpy.types.Curve],
# Optionally apply a vector offset to all coordinates
coordinate_offset: Optional[Vector] = None,
# How many representation items to create
total_items: int = 1,
# A scale factor to apply for all vectors in case the unit is different
unit_scale: Optional[float] = None,
# If we should force faceted breps for meshes
should_force_faceted_brep: bool = False,
# If we should force triangulation for meshes
should_force_triangulation: bool = False,
# If UV coordinates should also be generated
should_generate_uvs: bool = False,
# Whether to cast a mesh into a particular class
ifc_representation_class: Optional[
Literal[
"IfcExtrudedAreaSolid/IfcRectangleProfileDef",
"IfcExtrudedAreaSolid/IfcCircleProfileDef",
"IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef",
"IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids",
"IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage",
"IfcGeometricCurveSet/IfcTextLiteral",
"IfcTextLiteral",
]
] = None,
# The material profile set if the extrusion requires it
profile_set_usage: Optional[ifcopenshell.entity_instance] = None,
# The text literal if the representation requires it
text_literal: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance:
# lazy import Helper to avoid circular import
if "Helper" not in globals():
from blenderbim.bim.module.geometry.helper import Helper
globals()["Helper"] = Helper
usecase = Usecase()
# TODO: This usecase currently depends on Blender's data model
usecase.file = file
usecase.settings = {
"context": context,
"blender_object": blender_object,
"geometry": geometry,
"coordinate_offset": coordinate_offset,
"total_items": total_items,
"unit_scale": unit_scale,
"should_force_faceted_brep": should_force_faceted_brep,
"should_force_triangulation": should_force_triangulation,
"should_generate_uvs": should_generate_uvs,
"ifc_representation_class": ifc_representation_class,
"profile_set_usage": profile_set_usage,
"text_literal": text_literal,
}
usecase.ifc_vertices = []
return usecase.execute()
class Usecase:
def __init__(self, file, **settings):
# TODO: This usecase currently depends on Blender's data model
self.file = file
self.settings = {
"context": None, # IfcGeometricRepresentationContext
"blender_object": None, # This is (currently) a Blender object, hence this depends on Blender now
"geometry": None, # This is (currently) a Blender data object, hence this depends on Blender now
"coordinate_offset": None, # Optionally apply a vector offset to all coordinates
"total_items": 1, # How many representation items to create
"unit_scale": None, # A scale factor to apply for all vectors in case the unit is different
"should_force_faceted_brep": False, # If we should force faceted breps for meshes
"should_force_triangulation": False, # If we should force triangulation for meshes
"should_generate_uvs": False, # If UV coordinates should also be generated
# Possible IFC representation classes:
# IfcExtrudedAreaSolid/IfcRectangleProfileDef
# IfcExtrudedAreaSolid/IfcCircleProfileDef
# IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef
# IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids
# IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage
# IfcGeometricCurveSet/IfcTextLiteral
# IfcTextLiteral
"ifc_representation_class": None, # Whether to cast a mesh into a particular class
"profile_set_usage": None, # The material profile set if the extrusion requires it
"text_literal": None, # The text literal if the representation requires it
}
self.ifc_vertices = []
for key, value in settings.items():
self.settings[key] = value
file: ifcopenshell.file
settings: dict[str, Any]
def execute(self):
self.is_manifold = None
@@ -345,12 +388,10 @@ class Usecase:
)
def create_curve3d_representation(self):
return self.file.createIfcShapeRepresentation(
self.settings["context"],
self.settings["context"].ContextIdentifier,
"Curve3D",
self.create_curves(),
)
if curves := self.create_curves():
return self.file.createIfcShapeRepresentation(
self.settings["context"], self.settings["context"].ContextIdentifier, "Curve3D", curves
)
def create_curve2d_representation(self):
return self.file.createIfcShapeRepresentation(
@@ -374,12 +415,14 @@ class Usecase:
return items
def create_plane(self, polygon):
return self.file.createIfcPlane(Position=self.file.createIfcAxis2Placement3D(
Location=self.file.createIfcCartesianPoint(polygon.center),
Axis=self.file.createIfcDirection(polygon.normal),
))
return self.file.createIfcPlane(
Position=self.file.createIfcAxis2Placement3D(
Location=self.file.createIfcCartesianPoint(polygon.center),
Axis=self.file.createIfcDirection(polygon.normal),
)
)
def create_annotation_fill_areas(self, is_2d=False):
def create_annotation_fill_areas(self, is_2d=False) -> list[ifcopenshell.entity_instance]:
items = []
if self.file.schema != "IFC2X3":
points = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices, is_2d=is_2d)
@@ -391,7 +434,9 @@ class Usecase:
items.append(self.file.createIfcAnnotationFillArea(OuterBoundary=curve))
return items
def create_curve_from_polygon(self, points, polygon, is_2d=False):
def create_curve_from_polygon(
self, points: ifcopenshell.entity_instance, polygon: bpy.types.MeshPolygon, is_2d=False
) -> ifcopenshell.entity_instance:
indices = list(polygon.vertices)
indices.append(indices[0])
edge_loop = [self.file.createIfcLineIndex((v1 + 1, v2 + 1)) for v1, v2 in zip(indices, indices[1:])]
@@ -414,7 +459,7 @@ class Usecase:
results.append(self.file.createIfcSweptDiskSolid(curve, radius))
return results
def is_mesh_curve_consequtive(self, geom_data):
def is_mesh_curve_consecutive(self, geom_data):
import blenderbim.tool as tool
bm = tool.Blender.get_bmesh_for_mesh(geom_data)
@@ -460,15 +505,14 @@ class Usecase:
return False
return True
def create_curves(self, should_exclude_faces=False, is_2d=False):
def create_curves(self, should_exclude_faces=False, is_2d=False, ignore_non_loose_edges=False):
geom_data = self.settings["geometry"]
if isinstance(geom_data, bpy.types.Mesh):
if self.is_mesh_curve_consequtive(geom_data):
if self.is_mesh_curve_consecutive(geom_data):
if self.file.schema == "IFC2X3":
return self.create_curves_from_mesh_ifc2x3(should_exclude_faces=should_exclude_faces, is_2d=is_2d)
else:
return self.create_curves_from_mesh(should_exclude_faces=should_exclude_faces, is_2d=is_2d)
return self.create_curves_from_mesh(should_exclude_faces=should_exclude_faces, is_2d=is_2d)
import blenderbim.tool as tool
@@ -530,7 +574,9 @@ class Usecase:
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
tool.Blender.apply_bmesh(mesh, bm)
def create_curves_from_mesh_ifc2x3(self, should_exclude_faces=False, is_2d=False):
def create_curves_from_mesh_ifc2x3(
self, should_exclude_faces=False, is_2d=False
) -> list[ifcopenshell.entity_instance]:
geom_data = self.settings["geometry"].copy()
self.remove_doubles_from_mesh(geom_data)
curves = []
@@ -568,7 +614,7 @@ class Usecase:
curve_object_data = self.settings["geometry"]
dim = (lambda v: v.xy) if is_2d else (lambda v: v.xyz)
results = []
for spline in self.settings["geometry"].splines:
for spline in curve_object_data.splines:
points = spline.bezier_points[:] + spline.points[:]
if spline.use_cyclic_u:
points.append(points[0])
@@ -741,9 +787,7 @@ class Usecase:
[uv + 1 for uv in polygon.loop_indices]
)
coordinates = self.file.createIfcCartesianPointList3D(
[self.convert_si_to_unit(v.co) for v in self.settings["geometry"].vertices]
)
coordinates = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices)
if self.settings["should_generate_uvs"]:
# Blender supports multiple UV layers. We don't. Too bad.
@@ -778,9 +822,7 @@ class Usecase:
ifc_raw_items[polygon.material_index % self.settings["total_items"]].append(
self.file.createIfcIndexedPolygonalFace([v + 1 for v in polygon.vertices])
)
coordinates = self.file.createIfcCartesianPointList3D(
[self.convert_si_to_unit(v.co) for v in self.settings["geometry"].vertices]
)
coordinates = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices)
items = [self.file.createIfcPolygonalFaceSet(coordinates, self.is_manifold, i) for i in ifc_raw_items if i]
return self.file.createIfcShapeRepresentation(
self.settings["context"],
@@ -802,7 +844,12 @@ class Usecase:
]
)
def create_cartesian_point(self, x, y, z=None):
def create_cartesian_point(self, x, y, z=None, is_model_coords=True):
if is_model_coords and self.settings["coordinate_offset"]:
x += self.settings["coordinate_offset"][0]
y += self.settings["coordinate_offset"][1]
if z:
z += self.settings["coordinate_offset"][2]
x = self.convert_si_to_unit(x)
y = self.convert_si_to_unit(y)
if z is None:
@@ -810,14 +857,18 @@ class Usecase:
z = self.convert_si_to_unit(z)
return self.file.createIfcCartesianPoint((x, y, z))
def create_cartesian_point_list_from_vertices(self, vertices, is_2d=False):
def create_cartesian_point_list_from_vertices(self, vertices: list[bpy.types.MeshVertex], is_2d=False, is_model_coords=True):
if is_model_coords and self.settings["coordinate_offset"]:
if is_2d:
xy_offset = Vector((self.settings["coordinate_offset"][0:2]))
return self.file.createIfcCartesianPointList2D([self.convert_si_to_unit(v.co.xy + xy_offset) for v in vertices])
xyz_offset = Vector((self.settings["coordinate_offset"][0:3]))
return self.file.createIfcCartesianPointList3D([self.convert_si_to_unit(v.co.xyz + xyz_offset) for v in vertices])
if is_2d:
return self.file.createIfcCartesianPointList2D([self.convert_si_to_unit(v.co.xy) for v in vertices])
return self.file.createIfcCartesianPointList3D([self.convert_si_to_unit(v.co) for v in vertices])
def convert_si_to_unit(self, co):
if self.settings["coordinate_offset"]:
return (co / self.settings["unit_scale"]) + self.settings["coordinate_offset"]
return co / self.settings["unit_scale"]
def create_annotation2d_representation(self):
@@ -17,23 +17,36 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.unit
from ifcopenshell.util.data import Clipping
from math import sin, cos
from typing import Any, Optional, Union
def add_slab_representation(
file,
# IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
# in meters
depth: float = 0.2,
# in radians
x_angle: float = 0.0,
# A list of planes that define clipping half space solids
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None,
) -> ifcopenshell.entity_instance:
usecase = Usecase()
usecase.file = file
usecase.settings = {
"context": context,
"depth": depth,
"x_angle": x_angle,
"clippings": clippings if clippings is not None else [],
}
return usecase.execute()
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"context": None, # IfcGeometricRepresentationContext
"depth": 0.2,
"x_angle": 0, # Radians
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
"clippings": [], # A list of planes that define clipping half space solids
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
return self.file.createIfcShapeRepresentation(
@@ -18,28 +18,43 @@
import ifcopenshell.util.unit
from math import sin, cos
from typing import Optional, Union, Any
from ifcopenshell.util.data import Clipping
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"context": None, # IfcGeometricRepresentationContext
"length": 1.0,
"height": 3.0,
"offset": 0.0,
"thickness": 0.2,
# Sloped walls along the wall's X axis, provided in radians
"x_angle": 0,
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
"clippings": [], # A list of planes that define clipping half space solids
"booleans": [], # Any existing IfcBooleanResults
}
for key, value in settings.items():
self.settings[key] = value
def add_wall_representation(
file: ifcopenshell.file,
context: ifcopenshell.entity_instance, # IfcGeometricRepresentationContext
# all lengths are in meters
length: float = 1.0,
height: float = 3.0,
offset: float = 0.0,
thickness: float = 0.2,
# Sloped walls along the wall's X axis, provided in radians
x_angle: float = 0.0,
# A list of planes that define clipping half space solids
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None,
# Any existing IfcBooleanResults
booleans: Optional[list[ifcopenshell.entity_instance]] = None,
) -> ifcopenshell.entity_instance:
usecase = Usecase()
usecase.file = file
usecase.settings = {
"context": context,
"length": length,
"height": height,
"offset": offset,
"thickness": thickness,
"x_angle": x_angle,
"clippings": clippings if clippings is not None else [],
"booleans": booleans if booleans is not None else [],
}
return usecase.execute()
class Usecase:
def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
self.settings["clippings"] = [Clipping.parse(c) for c in self.settings["clippings"]]
@@ -16,11 +16,14 @@
# 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 __future__ import annotations
import collections.abc
import ifcopenshell.util.unit
from ifcopenshell.util.shape_builder import ShapeBuilder, V
from itertools import chain
from mathutils import Vector
import collections
import dataclasses
from typing import Any, Optional, Literal, Union
# SCHEMAS describe panels setup
@@ -42,6 +45,11 @@ DEFAULT_PANEL_SCHEMAS = {
}
def mm(x: float) -> float:
"""mm to meters shortcut for readability"""
return x / 1000
def create_ifc_window_frame_simple(
builder: ShapeBuilder, size: Vector, thickness: list, position: Vector = V(0, 0, 0).freeze()
):
@@ -56,12 +64,7 @@ def create_ifc_window_frame_simple(
th_left, th_up, th_right, th_bottom = thickness
def get_extruded_profile(profile):
return builder.extrude(
profile,
size.y,
position=position,
**builder.extrude_kwargs("Y")
)
return builder.extrude(profile, size.y, position=position, **builder.extrude_kwargs("Y"))
# if all lining sides are present then we can just use two rectangles
# as inner and outer curves of the profile
@@ -207,12 +210,7 @@ def create_ifc_window(
glass_position = frame_position + V(0, frame_size.y / 2 - glass_thickness / 2, 0)
glass_rect = builder.deep_copy(frame_extruded_items[0].SweptArea.InnerCurves[0])
glass = builder.extrude(
glass_rect,
glass_thickness,
position=glass_position,
**builder.extrude_kwargs("Y")
)
glass = builder.extrude(glass_rect, glass_thickness, position=glass_position, **builder.extrude_kwargs("Y"))
output_items = [lining_items, frame_extruded_items, [glass]]
builder.translate(chain(*output_items), position)
@@ -220,73 +218,214 @@ def create_ifc_window(
return output_items
class Usecase:
def __init__(self, file, **settings):
"""units in settings expected to be in ifc project units"""
self.file = file
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindow.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowTypePartitioningEnum.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowLiningProperties.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowPanelProperties.htm
self.settings = {"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(self.file)}
self.settings.update(
{
"context": None, # IfcGeometricRepresentationContext
# SINGLE_PANEL, DOUBLE_PANEL_HORIZONTAL, DOUBLE_PANEL_VERTICAL,
# TRIPLE_PANEL_BOTTOM, TRIPLE_PANEL_HORIZONTAL, TRIPLE_PANEL_LEFT,
# TRIPLE_PANEL_RIGHT, TRIPLE_PANEL_TOP, TRIPLE_PANEL_VERTICAL
"partition_type": "SINGLE_PANEL",
"overall_height": self.convert_si_to_unit(0.9),
"overall_width": self.convert_si_to_unit(0.6),
"lining_properties": {
"LiningDepth": self.convert_si_to_unit(0.050),
"LiningThickness": self.convert_si_to_unit(0.050),
"LiningOffset": self.convert_si_to_unit(0.050), # offset to the wall
# offset from the wall
"LiningToPanelOffsetX": self.convert_si_to_unit(0.025),
# offset from the lining
# that way it allows you to define overall_depth constant between all panels
# and still have panels with different size:
# overall_depth = lining_depth + offset_y
# full offset from X axis = overall_depth - frame_depth
"LiningToPanelOffsetY": self.convert_si_to_unit(0.025),
# applies to DoublePanelVertical, TriplePanelBottom, TriplePanelTop,
# TriplePanelLeft, TriplePanelRight
# mullion - horizontal distance between panels
"MullionThickness": self.convert_si_to_unit(0.050),
# distance from the first lining to the mullion center
"FirstMullionOffset": self.convert_si_to_unit(0.3),
# applies to TriplePanelVertical
# distance from the first lining to the second mullion center
"SecondMullionOffset": self.convert_si_to_unit(0.45),
# applies to DoublePanelHorizontal, TriplePanelBottom, TriplePanelTop,
# TriplePanelLeft, TriplePanelRight
# works similar way to mullion
"TransomThickness": self.convert_si_to_unit(0.050),
"FirstTransomOffset": self.convert_si_to_unit(0.3),
# applies to TriplePanelHorizontal
"SecondTransomOffset": self.convert_si_to_unit(0.6),
"ShapeAspectStyle": None, # DEPRECATED
},
"panel_properties": [
{
"FrameDepth": self.convert_si_to_unit(0.035), # by Y
"FrameThickness": self.convert_si_to_unit(0.035), # by X
# BOTTOM, LEFT, MIDDLE, RIGHT, TOP
"PanelPosition": ..., # NEVER USED
# defines the basic ways to describe how window panels operate
# how it's hanged, how it opens
"OperationType": None, # NEVER USED
"ShapeAspectStyle": None, # DEPRECATED
},
],
}
# we use dataclass as we need default values for arguments
# it's okay to use slots since we don't need dynamic attributes
@dataclasses.dataclass(slots=True)
class WindowLiningProperties:
LiningDepth: Optional[float] = None
"""Optional, defaults to 50mm."""
LiningThickness: Optional[float] = None
"""Optional, defaults to 50mm."""
LiningOffset: Optional[float] = None
"""Offset to the wall. Optional, defaults to 50mm."""
LiningToPanelOffsetX: Optional[float] = None
"""Offset from the wall. Optional, defaults to 25mm."""
# that way it allows you to define overall_depth constant between all panels
# and still have panels with different size:
# overall_depth = lining_depth + offset_y
# full offset from X axis = overall_depth - frame_depth.
LiningToPanelOffsetY: Optional[float] = None
"""Offset from the lining. Optional, defaults to 25mm."""
MullionThickness: Optional[float] = None
"""Mullion thickness (horizontal distance between panels).
Applies to windows of types: DoublePanelVertical, TriplePanelBottom, TriplePanelTop,
TriplePanelLeft, TriplePanelRight.
Optional, defaults to 50mm."""
FirstMullionOffset: Optional[float] = None
"""Distance from the first lining to the mullion center. Optional, defaults to 300mm."""
SecondMullionOffset: Optional[float] = None
"""Distance from the first lining to the second mullion center.
Applies to windows of type: TriplePanelVertical.
Optional, defaults to 450mm."""
TransomThickness: Optional[float] = None
"""Transom thickness (vertical distance between panels), works similar way to mullions.
Applies to windows of types:DoublePanelHorizontal, TriplePanelBottom, TriplePanelTop,
TriplePanelLeft, TriplePanelRight.
Optional, defaults to 50mm."""
FirstTransomOffset: Optional[float] = None
"""Optional, defaults to 300mm."""
SecondTransomOffset: Optional[float] = None
"""
Applies to windows of type: TriplePanelHorizontal.
Optional, defaults to 600mm."""
ShapeAspectStyle: None = None
"""Optional. Deprecated argument."""
def initialize_properties(self, unit_scale: float) -> None:
# in meters
# fmt: off
default_values: dict[str, float] = dict(
LiningDepth = mm(50),
LiningThickness = mm(50),
LiningOffset = mm(50),
LiningToPanelOffsetX = mm(25),
LiningToPanelOffsetY = mm(25),
MullionThickness = mm(50),
FirstMullionOffset = mm(300),
SecondMullionOffset = mm(450),
TransomThickness = mm(50),
FirstTransomOffset = mm(300),
SecondTransomOffset = mm(600),
)
# fmt: on
for key, value in settings.items():
self.settings[key] = value
self.settings["panel_schema"] = DEFAULT_PANEL_SCHEMAS[self.settings["partition_type"]]
si_conversion = 1 / unit_scale
for attr, default_value in default_values.items():
if getattr(self, attr) is not None:
continue
setattr(self, attr, default_value * si_conversion)
@dataclasses.dataclass(slots=True)
class WindowPanelProperties:
FrameDepth: Optional[float] = None
"""Frame thickness by Y axis. Optional, defaults to 35 mm."""
FrameThickness: Optional[float] = None
"""Frame thickness by X axis. Optional, defaults to 35 mm."""
PanelPosition: None = None
"""Optional, value is never used"""
PanelOperation: None = None
"""Optional, value is never used.
Defines the basic ways to describe how window panels operate."""
ShapeAspectStyle: None = None
"""Optional. Deprecated argument."""
def initialize_properties(self, unit_scale: float) -> None:
# in meters
# fmt: off
default_values: dict[str, float] = dict(
FrameDepth = mm(35),
FrameThickness = mm(35),
)
# fmt: on
si_conversion = 1 / unit_scale
for attr, default_value in default_values.items():
if getattr(self, attr) is not None:
continue
setattr(self, attr, default_value * si_conversion)
def add_window_representation(
file: ifcopenshell.file,
*, # keywords only as this API implementation is probably not final
context: ifcopenshell.entity_instance,
overall_height: Optional[float] = None,
overall_width: Optional[float] = None,
partition_type: Literal[
"SINGLE_PANEL",
"DOUBLE_PANEL_HORIZONTAL",
"DOUBLE_PANEL_VERTICAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_HORIZONTAL",
"TRIPLE_PANEL_LEFT",
"TRIPLE_PANEL_RIGHT",
"TRIPLE_PANEL_TOP",
"TRIPLE_PANEL_VERTICAL",
] = "SINGLE_PANEL",
lining_properties: Optional[Union[WindowLiningProperties, dict[str, Any]]] = None,
panel_properties: Optional[list[Union[WindowPanelProperties, dict[str, Any]]]] = None,
unit_scale: Optional[float] = None,
) -> ifcopenshell.entity_instance:
"""units in usecase_settings expected to be in ifc project units
:param context: IfcGeometricRepresentationContext for the representation.
:type context: ifcopenshell.entity_instance
:param overall_height: Overall window height. Defaults to 0.9m.
:type overall_height: float, optional
:param overall_width: Overall window width. Defaults to 0.6m.
:type overall_width: float, optional
:param partition_type: Type of the window. Defaults to SINGLE_PANEL.
:type partition_type: str, optional
:param lining_properties: WindowLiningProperties or a dictionary to create one.
See WindowLiningProperties description for details.
:type lining_properties: Union[WindowLiningProperties, dict[str, Any]]]
:param panel_properties: A list of WindowPanelProperties or dictionaries to create one.
See WindowPanelProperties description for details.
:type panel_properties: list[Union[WindowPanelProperties, dict[str, Any]]]]
:param unit_scale: The unit scale as calculated by
ifcopenshell.util.unit.calculate_unit_scale. If not provided, it
will be automatically calculated for you.
:type unit_scale: float, optional
:return: IfcShapeRepresentation for a window.
:rtype: ifcopenshell.entity_instance
"""
usecase = Usecase()
usecase.file = file
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindow.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowTypePartitioningEnum.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowLiningProperties.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowPanelProperties.htm
# define unit_scale first as it's going to be used setting default arguments
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) if unit_scale is None else unit_scale
settings: dict[str, Any] = {"unit_scale": unit_scale}
if lining_properties is None:
lining_properties = WindowLiningProperties()
elif not isinstance(lining_properties, WindowLiningProperties):
lining_properties = WindowLiningProperties(**lining_properties)
lining_properties.initialize_properties(unit_scale)
lining_properties = dataclasses.asdict(lining_properties)
if panel_properties is None:
panel_properties = [WindowPanelProperties()]
for i in range(len(panel_properties)):
properties = panel_properties[i]
if not isinstance(properties, WindowPanelProperties):
properties = WindowPanelProperties(**properties)
properties.initialize_properties(unit_scale)
panel_properties[i] = dataclasses.asdict(properties)
settings.update(
{
"context": context,
"overall_height": overall_height if overall_height is not None else usecase.convert_si_to_unit(0.9),
"overall_width": overall_width if overall_width is not None else usecase.convert_si_to_unit(0.6),
"partition_type": partition_type,
"lining_properties": lining_properties,
"panel_properties": panel_properties,
}
)
usecase.settings = settings
usecase.settings["panel_schema"] = DEFAULT_PANEL_SCHEMAS[usecase.settings["partition_type"]]
return usecase.execute()
class Usecase:
def execute(self):
builder = ShapeBuilder(self.file)
overall_height = self.settings["overall_height"]
@@ -20,13 +20,16 @@ import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"product": None, "representation": None}
for key, value in settings.items():
self.settings[key] = value
def assign_representation(
file: ifcopenshell.file, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
) -> None:
usecase = Usecase()
usecase.file = file
usecase.settings = {"product": product, "representation": representation}
return usecase.execute()
class Usecase:
def execute(self):
if self.settings["product"].is_a("IfcProduct"):
product_type = ifcopenshell.util.element.get_type(self.settings["product"])
@@ -18,47 +18,49 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.element
from typing import Optional
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"relating_element": None,
"related_element": None,
"description": None,
}
for key, value in settings.items():
self.settings[key] = value
def connect_element(
file: ifcopenshell.file,
relating_element: ifcopenshell.entity_instance,
related_element: ifcopenshell.entity_instance,
description: Optional[str] = None,
) -> ifcopenshell.entity_instance:
settings = {
"relating_element": relating_element,
"related_element": related_element,
"description": description,
}
def execute(self):
incompatible_connections = []
incompatible_connections = []
for rel in self.settings["relating_element"].ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == self.settings["related_element"]:
incompatible_connections.append(rel)
for rel in settings["relating_element"].ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == settings["related_element"]:
incompatible_connections.append(rel)
for rel in self.settings["related_element"].ConnectedTo:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == self.settings["relating_element"]:
incompatible_connections.append(rel)
for rel in settings["related_element"].ConnectedTo:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == settings["relating_element"]:
incompatible_connections.append(rel)
if incompatible_connections:
for connection in set(incompatible_connections):
history = connection.OwnerHistory
self.file.remove(connection)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
if incompatible_connections:
for connection in set(incompatible_connections):
history = connection.OwnerHistory
file.remove(connection)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
for rel in self.settings["relating_element"].ConnectedTo:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == self.settings["related_element"]:
rel.Description = self.settings["description"]
return rel
for rel in settings["relating_element"].ConnectedTo:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == settings["related_element"]:
rel.Description = settings["description"]
return rel
return self.file.createIfcRelConnectsElements(
ifcopenshell.guid.new(),
OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file),
Description=self.settings["description"],
RelatingElement=self.settings["relating_element"],
RelatedElement=self.settings["related_element"],
)
return file.createIfcRelConnectsElements(
ifcopenshell.guid.new(),
OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", file),
Description=settings["description"],
RelatingElement=settings["relating_element"],
RelatedElement=settings["related_element"],
)
@@ -18,79 +18,83 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.element
from typing import Optional
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"relating_element": None,
"related_element": None,
"relating_connection": "NOTDEFINED",
"related_connection": "NOTDEFINED",
"description": None,
}
for key, value in settings.items():
self.settings[key] = value
def connect_path(
file: ifcopenshell.file,
relating_element: ifcopenshell.entity_instance,
related_element: ifcopenshell.entity_instance,
relating_connection: str = "NOTDEFINED",
related_connection: str = "NOTDEFINED",
description: Optional[str] = None,
) -> ifcopenshell.entity_instance:
settings = {
"relating_element": relating_element,
"related_element": related_element,
"relating_connection": relating_connection,
"related_connection": related_connection,
"description": description,
}
def execute(self):
incompatible_connections = []
for rel in self.settings["relating_element"].ConnectedTo:
if not rel.is_a("IfcRelConnectsPathElements"):
continue
if rel.RelatedElement == self.settings["related_element"]:
incompatible_connections.append(rel)
elif (
rel.RelatingConnectionType in ["ATSTART", "ATEND"]
and rel.RelatingConnectionType == self.settings["relating_connection"]
):
incompatible_connections.append(rel)
incompatible_connections = []
for rel in settings["relating_element"].ConnectedTo:
if not rel.is_a("IfcRelConnectsPathElements"):
continue
if rel.RelatedElement == settings["related_element"]:
incompatible_connections.append(rel)
elif (
rel.RelatingConnectionType in ["ATSTART", "ATEND"]
and rel.RelatingConnectionType == settings["relating_connection"]
):
incompatible_connections.append(rel)
for rel in self.settings["relating_element"].ConnectedFrom:
if not rel.is_a("IfcRelConnectsPathElements"):
continue
if (
rel.RelatedConnectionType in ["ATSTART", "ATEND"]
and rel.RelatedConnectionType == self.settings["relating_connection"]
):
incompatible_connections.append(rel)
for rel in settings["relating_element"].ConnectedFrom:
if not rel.is_a("IfcRelConnectsPathElements"):
continue
if (
rel.RelatedConnectionType in ["ATSTART", "ATEND"]
and rel.RelatedConnectionType == settings["relating_connection"]
):
incompatible_connections.append(rel)
for rel in self.settings["related_element"].ConnectedFrom:
if not rel.is_a("IfcRelConnectsPathElements"):
continue
if (
rel.RelatedConnectionType in ["ATSTART", "ATEND"]
and rel.RelatedConnectionType == self.settings["related_connection"]
):
incompatible_connections.append(rel)
for rel in settings["related_element"].ConnectedFrom:
if not rel.is_a("IfcRelConnectsPathElements"):
continue
if (
rel.RelatedConnectionType in ["ATSTART", "ATEND"]
and rel.RelatedConnectionType == settings["related_connection"]
):
incompatible_connections.append(rel)
for rel in self.settings["related_element"].ConnectedTo:
if not rel.is_a("IfcRelConnectsPathElements"):
continue
if rel.RelatedElement == self.settings["relating_element"]:
incompatible_connections.append(rel)
elif (
rel.RelatingConnectionType in ["ATSTART", "ATEND"]
and rel.RelatingConnectionType == self.settings["related_connection"]
):
incompatible_connections.append(rel)
for rel in settings["related_element"].ConnectedTo:
if not rel.is_a("IfcRelConnectsPathElements"):
continue
if rel.RelatedElement == settings["relating_element"]:
incompatible_connections.append(rel)
elif (
rel.RelatingConnectionType in ["ATSTART", "ATEND"]
and rel.RelatingConnectionType == settings["related_connection"]
):
incompatible_connections.append(rel)
if incompatible_connections:
for connection in set(incompatible_connections):
history = connection.OwnerHistory
self.file.remove(connection)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
if incompatible_connections:
for connection in set(incompatible_connections):
history = connection.OwnerHistory
file.remove(connection)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
return self.file.createIfcRelConnectsPathElements(
ifcopenshell.guid.new(),
OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file),
Description=self.settings["description"],
RelatingElement=self.settings["relating_element"],
RelatedElement=self.settings["related_element"],
RelatingConnectionType=self.settings["relating_connection"],
RelatedConnectionType=self.settings["related_connection"],
RelatingPriorities=[],
RelatedPriorities=[],
)
return file.createIfcRelConnectsPathElements(
ifcopenshell.guid.new(),
OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", file),
Description=settings["description"],
RelatingElement=settings["relating_element"],
RelatedElement=settings["related_element"],
RelatingConnectionType=settings["relating_connection"],
RelatedConnectionType=settings["related_connection"],
RelatingPriorities=[],
RelatedPriorities=[],
)
@@ -21,20 +21,33 @@ import ifcopenshell.api
import ifcopenshell.util.unit
class Usecase:
def __init__(self, file, element=None, context=None, p1=None, p2=None, elevation=None, height=None, thickness=None, is_si=True):
self.file = file
self.settings = {
"element": element,
"context": context,
"p1": p1,
"p2": p2,
"elevation": elevation,
"height": height,
"thickness": thickness,
"is_si": is_si
}
def create_2pt_wall(
file: ifcopenshell.file,
element: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
p1: tuple[float, float],
p2: tuple[float, float],
elevation: float,
height: float,
thickness: float,
is_si: bool = True,
) -> ifcopenshell.entity_instance:
usecase = Usecase()
usecase.file = file
usecase.settings = {
"element": element,
"context": context,
"p1": p1,
"p2": p2,
"elevation": elevation,
"height": height,
"thickness": thickness,
"is_si": is_si,
}
return usecase.execute()
class Usecase:
def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -44,9 +57,9 @@ class Usecase:
length = float(np.linalg.norm(self.settings["p2"] - self.settings["p1"]))
if not self.settings["is_si"]:
length=self.convert_unit_to_si(length)
self.settings["height"]=self.convert_unit_to_si(self.settings["height"])
self.settings["thickness"]=self.convert_unit_to_si(self.settings["thickness"])
length = self.convert_unit_to_si(length)
self.settings["height"] = self.convert_unit_to_si(self.settings["height"])
self.settings["thickness"] = self.convert_unit_to_si(self.settings["thickness"])
self.settings["p1"][0] = self.convert_unit_to_si(self.settings["p1"][0])
self.settings["p1"][1] = self.convert_unit_to_si(self.settings["p1"][1])
self.settings["elevation"] = self.convert_unit_to_si(self.settings["elevation"])
@@ -20,38 +20,36 @@ import ifcopenshell
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"relating_element": None,
"related_element": None,
}
for key, value in settings.items():
self.settings[key] = value
def disconnect_element(
file: ifcopenshell.file,
relating_element: ifcopenshell.entity_instance,
related_element: ifcopenshell.entity_instance,
) -> None:
# TODO: arguments relating_element, related_element probably
# should be renamed to element1, element2
# as api call doesn't really treat them as "relating" and "related"
# and just purging all connections between them
incompatible_connections = []
def execute(self):
incompatible_connections = []
for rel in relating_element.ConnectedTo:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == related_element:
incompatible_connections.append(rel)
for rel in self.settings["relating_element"].ConnectedTo:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == self.settings["related_element"]:
incompatible_connections.append(rel)
for rel in relating_element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == related_element:
incompatible_connections.append(rel)
for rel in self.settings["relating_element"].ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == self.settings["related_element"]:
incompatible_connections.append(rel)
for rel in related_element.ConnectedTo:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == relating_element:
incompatible_connections.append(rel)
for rel in self.settings["related_element"].ConnectedTo:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == self.settings["relating_element"]:
incompatible_connections.append(rel)
for rel in related_element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == relating_element:
incompatible_connections.append(rel)
for rel in self.settings["related_element"].ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == self.settings["relating_element"]:
incompatible_connections.append(rel)
if incompatible_connections:
for connection in set(incompatible_connections):
history = connection.OwnerHistory
self.file.remove(connection)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
if incompatible_connections:
for connection in set(incompatible_connections):
history = connection.OwnerHistory
file.remove(connection)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
@@ -19,40 +19,40 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
from typing import Optional
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"relating_element": None,
"related_element": None,
"element": None,
"connection_type": None,
}
for key, value in settings.items():
self.settings[key] = value
def disconnect_path(
file: ifcopenshell.file,
element: Optional[ifcopenshell.entity_instance] = None,
connection_type: Optional[str] = None,
relating_element: Optional[ifcopenshell.entity_instance] = None,
related_element: Optional[ifcopenshell.entity_instance] = None,
) -> None:
"""There are two options to use this API method:
- provide `element` (connected from) and `connection_type` that should be disconnected.
- provide connected elements to disconnect explicitly:
`relating_element` (connected from) and `related_element` (connected to)
"""
if connection_type and element:
connections = [
r
for r in element.ConnectedTo
if r.is_a("IfcRelConnectsPathElements") and r.RelatingConnectionType == connection_type
] + [
r
for r in element.ConnectedFrom
if r.is_a("IfcRelConnectsPathElements") and r.RelatedConnectionType == connection_type
]
elif related_element:
connections = [
r
for r in relating_element.ConnectedTo
if r.is_a("IfcRelConnectsPathElements") and r.RelatedElement == related_element
]
def execute(self):
if self.settings["connection_type"] and self.settings["element"]:
connections = [
r
for r in self.settings["element"].ConnectedTo
if r.is_a("IfcRelConnectsPathElements") and r.RelatingConnectionType == self.settings["connection_type"]
] + [
r
for r in self.settings["element"].ConnectedFrom
if r.is_a("IfcRelConnectsPathElements") and r.RelatedConnectionType == self.settings["connection_type"]
]
else:
connections = [
r
for r in self.settings["relating_element"].ConnectedTo
if r.is_a("IfcRelConnectsPathElements") and r.RelatedElement == self.settings["related_element"]
]
for connection in set(connections):
history = connection.OwnerHistory
self.file.remove(connection)
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
for connection in set(connections):
history = connection.OwnerHistory
file.remove(connection)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
@@ -17,19 +17,35 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import numpy as np
import numpy.typing as npt
import ifcopenshell.api
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.placement
from typing import Optional, Union
NPArrayOfFloats = npt.NDArray[np.float64]
def edit_object_placement(
file: ifcopenshell.file,
product: ifcopenshell.entity_instance,
matrix: Optional[NPArrayOfFloats] = None,
is_si: bool = True,
should_transform_children: bool = False,
) -> ifcopenshell.entity_instance:
usecase = Usecase()
usecase.file = file
usecase.settings = {
"product": product,
"matrix": matrix if matrix is not None else np.eye(4),
"is_si": is_si,
"should_transform_children": should_transform_children,
}
return usecase.execute()
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"product": None, "matrix": np.eye(4), "is_si": True, "should_transform_children": False}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if not hasattr(self.settings["product"], "ObjectPlacement"):
return
@@ -69,34 +85,37 @@ class Usecase:
return new_placement
def convert_matrix_to_si(self, matrix):
def convert_matrix_to_si(self, matrix: NPArrayOfFloats):
matrix[0][3] *= self.unit_scale
matrix[1][3] *= self.unit_scale
matrix[2][3] *= self.unit_scale
def get_placement_rel_to(self):
if getattr(self.settings["product"], "Decomposes", None):
relating_object = self.settings["product"].Decomposes[0].RelatingObject
return relating_object.ObjectPlacement if hasattr(relating_object, "ObjectPlacement") else None
elif getattr(self.settings["product"], "Nests", None):
relating_object = self.settings["product"].Nests[0].RelatingObject
return relating_object.ObjectPlacement if hasattr(relating_object, "ObjectPlacement") else None
elif getattr(self.settings["product"], "ContainedIn", None):
related_element = self.settings["product"].ContainedIn[0].RelatedElement
return related_element.ObjectPlacement if hasattr(related_element, "ObjectPlacement") else None
elif getattr(self.settings["product"], "VoidsElements", None):
relating_object = self.settings["product"].VoidsElements[0].RelatingBuildingElement
return relating_object.ObjectPlacement if hasattr(relating_object, "ObjectPlacement") else None
elif getattr(self.settings["product"], "FillsVoids", None):
relating_object = self.settings["product"].FillsVoids[0].RelatingOpeningElement
return relating_object.ObjectPlacement if hasattr(relating_object, "ObjectPlacement") else None
elif getattr(self.settings["product"], "ProjectsElements", None):
relating_object = self.settings["product"].ProjectsElements[0].RelatingElement
return relating_object.ObjectPlacement if hasattr(relating_object, "ObjectPlacement") else None
elif getattr(self.settings["product"], "ContainedInStructure", None):
return self.settings["product"].ContainedInStructure[0].RelatingStructure.ObjectPlacement
def get_placement_rel_to(self) -> Union[ifcopenshell.entity_instance, None]:
product = self.settings["product"]
relating_object = None
def get_children_settings(self, placement):
if rels := getattr(product, "Decomposes", None):
relating_object = rels[0].RelatingObject
elif rels := getattr(product, "Nests", None):
relating_object = rels[0].RelatingObject
elif rels := getattr(product, "ContainedIn", None):
relating_object = rels[0].RelatedElement
elif rels := getattr(product, "VoidsElements", None):
relating_object = rels[0].RelatingBuildingElement
elif rels := getattr(product, "FillsVoids", None):
relating_object = rels[0].RelatingOpeningElement
elif rels := getattr(product, "ProjectsElements", None):
relating_object = rels[0].RelatingElement
# TODO: add tests when there will be adherence api
elif rels := getattr(product, "AdheresToElement", None):
relating_object = rels[0].RelatingElement
elif rels := getattr(product, "ContainedInStructure", None):
return rels[0].RelatingStructure.ObjectPlacement
if relating_object:
return getattr(relating_object, "ObjectPlacement", None)
def get_children_settings(self, placement: Union[ifcopenshell.entity_instance, None]) -> list[dict]:
if not placement:
return []
results = []
@@ -116,7 +135,9 @@ class Usecase:
results.append({"product": obj, "matrix": matrix, "is_si": False, "should_transform_children": True})
return results
def get_relative_placement(self, placement_rel_to):
def get_relative_placement(
self, placement_rel_to: Union[ifcopenshell.entity_instance, None]
) -> ifcopenshell.entity_instance:
if placement_rel_to:
relating_object_matrix = ifcopenshell.util.placement.get_local_placement(placement_rel_to)
relating_object_matrix[0][3] = self.convert_unit_to_si(relating_object_matrix[0][3])
@@ -136,19 +157,21 @@ class Usecase:
relative_placement_matrix[:, 0][0:3],
)
def create_ifc_axis_2_placement_3d(self, point, up, forward):
def create_ifc_axis_2_placement_3d(
self, point: NPArrayOfFloats, up: NPArrayOfFloats, forward: NPArrayOfFloats
) -> ifcopenshell.entity_instance:
return self.file.createIfcAxis2Placement3D(
self.create_cartesian_point(point),
self.file.createIfcDirection(up.tolist()),
self.file.createIfcDirection(forward.tolist()),
)
def create_cartesian_point(self, co):
def create_cartesian_point(self, co: NPArrayOfFloats) -> ifcopenshell.entity_instance:
co = self.convert_si_to_unit(co)
return self.file.createIfcCartesianPoint(co.tolist())
def convert_si_to_unit(self, co):
def convert_si_to_unit(self, co: NPArrayOfFloats) -> NPArrayOfFloats:
return co / self.unit_scale
def convert_unit_to_si(self, co):
def convert_unit_to_si(self, co: NPArrayOfFloats) -> NPArrayOfFloats:
return co * self.unit_scale
@@ -16,16 +16,24 @@
# 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
from typing import Any
def map_representation(
file: ifcopenshell.file, representation: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
usecase = Usecase()
usecase.file = file
usecase.settings = {"representation": representation}
return usecase.execute()
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"representation": None}
self.ifc_vertices = []
for key, value in settings.items():
self.settings[key] = value
file: ifcopenshell.file
settings: dict[str, Any]
def execute(self):
def execute(self) -> ifcopenshell.entity_instance:
mapping_source = self.get_mapping_source()
zero = self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
@@ -45,7 +53,7 @@ class Usecase:
}
)
def get_mapping_source(self):
def get_mapping_source(self) -> ifcopenshell.entity_instance:
for inverse in self.file.get_inverse(self.settings["representation"]):
if inverse.is_a("IfcRepresentationMap"):
return inverse
@@ -19,13 +19,14 @@
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"item": None}
for key, value in settings.items():
self.settings[key] = value
def remove_boolean(file: ifcopenshell.file, item: ifcopenshell.entity_instance) -> None:
usecase = Usecase()
usecase.file = file
usecase.settings = {"item": item}
return usecase.execute()
class Usecase:
def execute(self):
item = None
for inverse in self.file.get_inverse(self.settings["item"]):
@@ -19,62 +19,57 @@
import ifcopenshell.util.element
class Usecase:
def __init__(self, file: ifcopenshell.file, representation: ifcopenshell.entity_instance):
"""Remove a representation.
def remove_representation(file: ifcopenshell.file, representation: ifcopenshell.entity_instance) -> None:
"""Remove a representation.
Also purges representation items and their related elements
like IfcStyledItem, tessellated facesets colours and UV map.
Also purges representation items and their related elements
like IfcStyledItem, tessellated facesets colours and UV map.
:param representation: IfcRepresentation to remove.
Note that it's expected that IfcRepresentation won't be in use
before calling this method (in such elements as IfcProductRepresentation, IfcShapeAspect)
otherwise representation won't be removed.
:type representation: ifcopenshell.entity_instance
:return: None
:rtype: None
"""
self.file = file
self.settings = {"representation": representation}
:param representation: IfcRepresentation to remove.
Note that it's expected that IfcRepresentation won't be in use
before calling this method (in such elements as IfcProductRepresentation, IfcShapeAspect)
otherwise representation won't be removed.
:type representation: ifcopenshell.entity_instance
:return: None
:rtype: None
"""
settings = {"representation": representation}
def execute(self) -> None:
styled_items = set()
presentation_layer_assignments = set()
textures = set()
colours = set()
for subelement in self.file.traverse(self.settings["representation"]):
if subelement.is_a("IfcRepresentationItem"):
[styled_items.add(s) for s in subelement.StyledByItem or []]
# IFC2X3 is using LayerAssignments
for s in (
subelement.LayerAssignment
if hasattr(subelement, "LayerAssignment")
else subelement.LayerAssignments
):
presentation_layer_assignments.add(s)
# IfcTessellatedFaceSet inverses
[textures.add(t) for t in getattr(subelement, "HasTextures", []) or []]
[colours.add(t) for t in getattr(subelement, "HasColours", []) or []]
elif subelement.is_a("IfcRepresentation"):
for layer in subelement.LayerAssignments:
presentation_layer_assignments.add(layer)
styled_items = set()
presentation_layer_assignments = set()
textures = set()
colours = set()
for subelement in file.traverse(settings["representation"]):
if subelement.is_a("IfcRepresentationItem"):
[styled_items.add(s) for s in subelement.StyledByItem or []]
# IFC2X3 is using LayerAssignments
for s in (
subelement.LayerAssignment if hasattr(subelement, "LayerAssignment") else subelement.LayerAssignments
):
presentation_layer_assignments.add(s)
# IfcTessellatedFaceSet inverses
[textures.add(t) for t in getattr(subelement, "HasTextures", []) or []]
[colours.add(t) for t in getattr(subelement, "HasColours", []) or []]
elif subelement.is_a("IfcRepresentation"):
for layer in subelement.LayerAssignments:
presentation_layer_assignments.add(layer)
ifcopenshell.util.element.remove_deep2(
self.file,
self.settings["representation"],
also_consider=list(styled_items | presentation_layer_assignments | colours),
do_not_delete=self.file.by_type("IfcGeometricRepresentationContext"),
)
ifcopenshell.util.element.remove_deep2(
file,
settings["representation"],
also_consider=list(styled_items | presentation_layer_assignments | colours),
do_not_delete=file.by_type("IfcGeometricRepresentationContext"),
)
for texture in textures:
ifcopenshell.util.element.remove_deep2(self.file, texture)
for colour in colours:
ifcopenshell.util.element.remove_deep2(self.file, colour)
for texture in textures:
ifcopenshell.util.element.remove_deep2(file, texture)
for colour in colours:
ifcopenshell.util.element.remove_deep2(file, colour)
to_delete = getattr(self.file, "to_delete", ())
for element in styled_items:
if not element.Item or element.Item in to_delete:
self.file.remove(element)
for element in presentation_layer_assignments:
if all(item in to_delete for item in element.AssignedItems):
self.file.remove(element)
to_delete = getattr(file, "to_delete", ())
for element in styled_items:
if not element.Item or element.Item in to_delete:
file.remove(element)
for element in presentation_layer_assignments:
if all(item in to_delete for item in element.AssignedItems):
file.remove(element)
@@ -20,13 +20,16 @@ import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"product": None, "representation": None}
for key, value in settings.items():
self.settings[key] = value
def unassign_representation(
file: ifcopenshell.file, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
) -> None:
usecase = Usecase()
usecase.file = file
usecase.settings = {"product": product, "representation": representation}
return usecase.execute()
class Usecase:
def execute(self):
if self.settings["product"].is_a("IfcProduct"):
self.unassign_product_representation(self.settings["product"], self.settings["representation"])
@@ -44,17 +47,25 @@ class Usecase:
product.Representation.Representations = representations
def unassign_type_representation(self):
matching_representation_map = None
representation_maps = self.settings["product"].RepresentationMaps or []
for representation_map in self.settings["product"].RepresentationMaps or []:
if representation_map.MappedRepresentation == self.settings["representation"]:
self.unassign_products_using_mapped_representation(representation_map)
self.remove_representation_map_only(representation_map)
matching_representation_map = representation_map
break
self.settings["product"].RepresentationMaps = self.settings["product"].RepresentationMaps or None
if matching_representation_map:
self.unassign_products_using_mapped_representation(matching_representation_map)
self.settings["product"].RepresentationMaps = [
rm for rm in self.settings["product"].RepresentationMaps if rm != matching_representation_map
] or None
self.remove_representation_map_only(matching_representation_map)
def remove_representation_map_only(self, representation_map):
representation_map.MappedRepresentation = self.file.createIfcShapeRepresentation()
ifcopenshell.util.element.remove_deep2(self.file, representation_map)
self.file.remove(representation_map)
def unassign_products_using_mapped_representation(self, representation_map):
mapped_representations = []
@@ -15,3 +15,23 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Manage georeferencing metadata
IFC model geometry may have a coordinate reference system (CRS) assigned to it.
It may also optionally have a map conversion defined to transform to and from
map coordinates and project local engineering coordinates.
"""
from .. import wrap_usecases
from .add_georeferencing import add_georeferencing
from .edit_georeferencing import edit_georeferencing
from .remove_georeferencing import remove_georeferencing
wrap_usecases(__path__, __name__)
__all__ = [
"add_georeferencing",
"edit_georeferencing",
"remove_georeferencing",
]
@@ -16,49 +16,48 @@
# 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
class Usecase:
def __init__(self, file):
"""Add empty georeferencing entities to a model
By default, models are not georeferenced. Georeferencing requires two
entities: a definition of the projected coordinated reference system
(CRS) used, and the transformation parameters between any local coordinate
system and that projected CRS if any.
def add_georeferencing(file: ifcopenshell.file) -> None:
"""Add empty georeferencing entities to a model
This function will create the entities to store the projected CRS and
map conversion transformation, but will leave all the parameters blank.
It is this the users responsibility to specify the correct
georeferencing parameters. See
ifcopenshell.api.georeference.edit_georeferencing.
By default, models are not georeferenced. Georeferencing requires two
entities: a definition of the projected coordinated reference system
(CRS) used, and the transformation parameters between any local coordinate
system and that projected CRS if any.
:return: None
:rtype: None
This function will create the entities to store the projected CRS and
map conversion transformation, but will leave all the parameters blank.
It is this the users responsibility to specify the correct
georeferencing parameters. See
ifcopenshell.api.georeference.edit_georeferencing.
Example:
:return: None
:rtype: None
.. code:: python
Example:
ifcopenshell.api.run("georeference.add_georeferencing", model)
"""
self.file = file
.. code:: python
def execute(self):
source_crs = None
for context in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.ContextType == "Model":
source_crs = context
break
if not source_crs:
return
projected_crs = self.file.create_entity("IfcProjectedCRS", **{"Name": ""})
self.file.create_entity(
"IfcMapConversion",
**{
"SourceCRS": source_crs,
"TargetCRS": projected_crs,
"Eastings": 0,
"Northings": 0,
"OrthogonalHeight": 0,
}
)
ifcopenshell.api.run("georeference.add_georeferencing", model)
"""
source_crs = None
for context in file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.ContextType == "Model":
source_crs = context
break
if not source_crs:
return
projected_crs = file.create_entity("IfcProjectedCRS", **{"Name": ""})
file.create_entity(
"IfcMapConversion",
**{
"SourceCRS": source_crs,
"TargetCRS": projected_crs,
"Eastings": 0,
"Northings": 0,
"OrthogonalHeight": 0,
}
)
@@ -16,78 +16,89 @@
# 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
from typing import Optional, Any
def edit_georeferencing(
file: ifcopenshell.file,
map_conversion: Optional[dict[str, Any]] = None,
projected_crs: Optional[dict[str, Any]] = None,
true_north: Optional[tuple[float, float]] = None,
) -> None:
"""Edits the attributes of a map conversion, projected CRS, and true north
Setting the correct georeferencing parameters is a complex topic and
should ideally be done with three parties present: the lead architect,
surveyor, and a third-party digital engineer with expertise in IFC to
moderate. For more information, read the BlenderBIM Add-on documentation
for Georeferencing:
https://docs.blenderbim.org/users/georeferencing.html
For more information about the attributes and data types of an
IfcMapConversion, consult the IFC documentation.
For more information about the attributes and data types of an
IfcProjectedCRS, consult the IFC documentation.
True north is defined as a unitised 2D vector pointing to true north.
Note that true north is not part of georeferencing, and is only
optionally provided as a reference value, typically for solar analysis.
See ifcopenshell.util.geolocation for more utilities to convert to and
from local and map coordinates to check your results.
:param map_conversion: The IfcMapConversion dictionary of attribute
names and values you want to edit.
:type map_conversion: dict, optional
:param projected_crs: The IfcProjectedCRS dictionary of attribute
names and values you want to edit.
:type projected_crs: dict, optional
:param true_north: A unitised 2D vector, where each ordinate is a float
:type true_north: tuple[float, float], optional
:return: None
:rtype: None
Example:
.. code:: python
ifcopenshell.api.run("georeference.add_georeferencing", model)
# This is the simplest scenario, a defined CRS (GDA2020 / MGA Zone
# 56, typically used in Sydney, Australia) but with no local
# coordinates. This is only recommended for horizontal construction
# projects, not for vertical construction (such as buildings).
ifcopenshell.api.run("georeference.edit_georeferencing", model,
projected_crs={"Name": "EPSG:7856"})
# For buildings, it is almost always recommended to specify map
# conversion parameters to a false origin and orientation to project
# north. See the diagram in the BlenderBIM Add-on Georeferencing
# documentation for correct calculation of the X Axis Abcissa and
# Ordinate.
ifcopenshell.api.run("georeference.edit_georeferencing", model,
projected_crs={"Name": "EPSG:7856"},
map_conversion={
"Eastings": 335087.17, # The architect nominates a false origin
"Northings": 6251635.41, # The architect nominates a false origin
# Note: this is the angle difference between Project North
# and Grid North. Remember: True North should never be used!
"XAxisAbscissa": cos(radians(-30)), # The architect nominates a project north
"XAxisOrdinate": sin(radians(-30)), # The architect nominates a project north
"Scale": 0.99956, # Ask your surveyor for your site's average combined scale factor!
})
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"map_conversion": map_conversion or {},
"projected_crs": projected_crs or {},
"true_north": true_north or [],
}
return usecase.execute()
class Usecase:
def __init__(self, file, map_conversion=None, projected_crs=None, true_north=None):
"""Edits the attributes of a map conversion, projected CRS, and true north
Setting the correct georeferencing parameters is a complex topic and
should ideally be done with three parties present: the lead architect,
surveyor, and a third-party digital engineer with expertise in IFC to
moderate. For more information, read the BlenderBIM Add-on documentation
for Georeferencing:
https://docs.blenderbim.org/users/georeferencing.html
For more information about the attributes and data types of an
IfcMapConversion, consult the IFC documentation.
For more information about the attributes and data types of an
IfcProjectedCRS, consult the IFC documentation.
True north is defined as a unitised 2D vector pointing to true north.
Note that true north is not part of georeferencing, and is only
optionally provided as a reference value, typically for solar analysis.
See ifcopenshell.util.geolocation for more utilities to convert to and
from local and map coordinates to check your results.
:param map_conversion: The IfcMapConversion dictionary of attribute
names and values you want to edit.
:type map_conversion: dict, optional
:param projected_crs: The IfcProjectedCRS dictionary of attribute
names and values you want to edit.
:type projected_crs: dict, optional
:param true_north: A unitised 2D vector, where each ordinate is a float
:type true_north: list[float]
:return: None
:rtype: None
Example:
.. code:: python
ifcopenshell.api.run("georeference.add_georeferencing", model)
# This is the simplest scenario, a defined CRS (GDA2020 / MGA Zone
# 56, typically used in Sydney, Australia) but with no local
# coordinates. This is only recommended for horizontal construction
# projects, not for vertical construction (such as buildings).
ifcopenshell.api.run("georeference.edit_georeferencing", model,
projected_crs={"Name": "EPSG:7856"})
# For buildings, it is almost always recommended to specify map
# conversion parameters to a false origin and orientation to project
# north. See the diagram in the BlenderBIM Add-on Georeferencing
# documentation for correct calculation of the X Axis Abcissa and
# Ordinate.
ifcopenshell.api.run("georeference.edit_georeferencing", model,
projected_crs={"Name": "EPSG:7856"},
map_conversion={
"Eastings": 335087.17, # The architect nominates a false origin
"Northings": 6251635.41, # The architect nominates a false origin
# Note: this is the angle difference between Project North
# and Grid North. Remember: True North should never be used!
"XAxisAbscissa": cos(radians(-30)), # The architect nominates a project north
"XAxisOrdinate": sin(radians(-30)), # The architect nominates a project north
"Scale": 0.99956, # Ask your surveyor for your site's average combined scale factor!
})
"""
self.file = file
self.settings = {
"map_conversion": map_conversion or {},
"projected_crs": projected_crs or {},
"true_north": true_north or [],
}
def execute(self):
map_conversion = self.file.by_type("IfcMapConversion")[0]
projected_crs = self.file.by_type("IfcProjectedCRS")[0]
@@ -98,7 +109,7 @@ class Usecase:
self.set_true_north()
def set_true_north(self):
if self.settings["true_north"] == []:
if not self.settings["true_north"]:
return
for context in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.TrueNorth:
@@ -108,6 +119,8 @@ class Usecase:
context.TrueNorth = self.file.create_entity("IfcDirection")
direction = context.TrueNorth
if self.settings["true_north"] is None:
# TODO: code will never be executed since None value
# is substituted by an empty list
context.TrueNorth = self.settings["true_north"]
elif context.CoordinateSpaceDimension == 2:
direction.DirectionRatios = self.settings["true_north"][0:2]
@@ -16,30 +16,29 @@
# 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
class Usecase:
def __init__(self, file):
"""Remove georeferencing data
All georeferencing parameters such as projected CRS and map conversion
data will be lost.
def remove_georeferencing(file: ifcopenshell.file) -> None:
"""Remove georeferencing data
:return: None
:rtype: None
All georeferencing parameters such as projected CRS and map conversion
data will be lost.
Example:
:return: None
:rtype: None
ifcopenshell.api.run("georeference.add_georeferencing", model)
# Let's change our mind
ifcopenshell.api.run("georeference.remove_georeferencing", model)
"""
self.file = file
Example:
def execute(self):
map_conversion = self.file.by_type("IfcMapConversion")[0]
projected_crs = self.file.by_type("IfcProjectedCRS")[0]
if projected_crs.MapUnit and len(self.file.get_inverse(projected_crs.MapUnit)) == 1:
# TODO: go deeper for conversion units
self.file.remove(projected_crs.MapUnit)
self.file.remove(projected_crs)
self.file.remove(map_conversion)
ifcopenshell.api.run("georeference.add_georeferencing", model)
# Let's change our mind
ifcopenshell.api.run("georeference.remove_georeferencing", model)
"""
map_conversion = file.by_type("IfcMapConversion")[0]
projected_crs = file.by_type("IfcProjectedCRS")[0]
if projected_crs.MapUnit and len(file.get_inverse(projected_crs.MapUnit)) == 1:
# TODO: go deeper for conversion units
file.remove(projected_crs.MapUnit)
file.remove(projected_crs)
file.remove(map_conversion)
@@ -15,3 +15,24 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Manages grid and grid axes
A grid in IFC may contain two or more axes running in two or more directions.
"""
from .. import wrap_usecases
try:
from .create_axis_curve import create_axis_curve
except ModuleNotFoundError as e:
print(f"Note: API not available due to missing dependencies: grid.create_axis_curve - {e}")
from .create_grid_axis import create_grid_axis
from .remove_grid_axis import remove_grid_axis
wrap_usecases(__path__, __name__)
__all__ = [
"create_axis_curve",
"create_grid_axis",
"remove_grid_axis",
]
@@ -16,52 +16,60 @@
# 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 __future__ import annotations
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.unit
import ifcopenshell.util.placement
from mathutils import Matrix # For now, we depend on Blender
import bpy.types
def create_axis_curve(
file: ifcopenshell.file, axis_curve: bpy.types.Object, grid_axis: ifcopenshell.entity_instance
) -> None:
"""Adds curve geometry to a grid axis to represent the axis extents
This currently depends on the Blender geometry kernel to function.
An IFC grid will have a minimum of two axes (typically perpendicular). Each
axis will then have a line which represents the extents of the axis.
:param axis_curve: The Blender object that contains a mesh data block with a
single edge.
:type axis_curve: bpy.types.Object
:param grid_axis: The IfcGridAxis element to add geometry to.
:type grid_axis: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
.. code:: python
# A pretty standard rectangular grid, with only two axes.
grid = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcGrid")
axis_a = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="A", uvw_axes="UAxes", grid=grid)
axis_1 = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="1", uvw_axes="VAxes", grid=grid)
# Assume you have these Blender objects in your active Blender session
obj1 = bpy.data.objects.get("AxisA")
obj2 = bpy.data.objects.get("Axis1")
ifcopenshell.api.run("grid.create_axis_curve", model, axis_curve=obj1, grid_axis=axis_a)
ifcopenshell.api.run("grid.create_axis_curve", model, axis_curve=obj2, grid_axis=axis_1)
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"axis_curve": axis_curve, # A Blender object
"grid_axis": grid_axis,
}
return usecase.execute()
class Usecase:
def __init__(self, file, axis_curve=None, grid_axis=None):
"""Adds curve geometry to a grid axis to represent the axis extents
This currently depends on the Blender geometry kernel to function.
An IFC grid will have a minimum of two axes (typically perpendicular). Each
axis will then have a line which represents the extents of the axis.
:param axis_curve: The Blender object that contains a mesh data block with a
single edge.
:type axis_curve: bpy.types.Object
:param grid_axis: The IfcGridAxis element to add geometry to.
:type grid_axis: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
Example:
.. code:: python
# A pretty standard rectangular grid, with only two axes.
grid = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcGrid")
axis_a = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="A", uvw_axes="UAxes", grid=grid)
axis_1 = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="1", uvw_axes="VAxes", grid=grid)
# Assume you have these Blender objects in your active Blender session
obj1 = bpy.data.objects.get("AxisA")
obj2 = bpy.data.objects.get("Axis1")
ifcopenshell.api.run("grid.create_axis_curve", model, axis_curve=obj1, grid_axis=axis_a)
ifcopenshell.api.run("grid.create_axis_curve", model, axis_curve=obj2, grid_axis=axis_1)
"""
self.file = file
self.settings = {
"axis_curve": axis_curve, # A Blender object
"grid_axis": grid_axis,
}
def execute(self):
existing_curve = self.settings["grid_axis"].AxisCurve
if existing_curve and len(self.file.get_inverse(existing_curve)) == 1:
@@ -15,71 +15,68 @@
#
# 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
from typing import Optional, Literal
class Usecase:
def __init__(self, file, axis_tag=None, same_sense=None, uvw_axes=None, grid=None):
"""Adds a new grid axis to a grid
def create_grid_axis(
file: ifcopenshell.file,
grid: ifcopenshell.entity_instance,
axis_tag: str = "A",
same_sense: bool = True,
uvw_axes: Literal["UAxes", "VAxes", "WAxes"] = "UAxes",
) -> ifcopenshell.entity_instance:
"""Adds a new grid axis to a grid
An IFC grid will typically have a minimum of two axes which will be
perpendicular to one another. Grids may be rectangular (typically
perpendicular lines), radial (where one set of axes is a circle and the
other is a line), or triangular (three sets of axes, each at a different
angle to one another).
An IFC grid will typically have a minimum of two axes which will be
perpendicular to one another. Grids may be rectangular (typically
perpendicular lines), radial (where one set of axes is a circle and the
other is a line), or triangular (three sets of axes, each at a different
angle to one another).
For a simple rectangular grid, the "UAxes" are a set of one or more
horizontal axes, which are typically labeled with the convention of A,
B, C, etc. The "VAxes" is another set of one or more vertical axes,
typically labeled with the convention of 1, 2, 3, etc. These axes are
horizontal or vertical relative to project north.
For a simple rectangular grid, the "UAxes" are a set of one or more
horizontal axes, which are typically labeled with the convention of A,
B, C, etc. The "VAxes" is another set of one or more vertical axes,
typically labeled with the convention of 1, 2, 3, etc. These axes are
horizontal or vertical relative to project north.
For a radial grid, the "UAxes" are straight lines, typically radiating
from a central point. The "VAxes" are circular perimeters, with the
center of these circles being the same central point.
For a radial grid, the "UAxes" are straight lines, typically radiating
from a central point. The "VAxes" are circular perimeters, with the
center of these circles being the same central point.
For a triangular grid, the UAxes, VAxes, and WAxes are all sets of one
or more straight lines.
For a triangular grid, the UAxes, VAxes, and WAxes are all sets of one
or more straight lines.
:param axis_tag: The name of the axis, that would typically be labeled
on drawings or described on site during coordination, such as A, B,
C, 1, 2, 3, etc. Defaults to "A".
:type axis_tag: str, optional
:param same_sense: Determines whether the direction of the axis's line
is reversed. True means the direction the geometry is defined in
represents the direction of the axis. False means the direction is
reversed. Leave as True if unsure. Defaults to "True".
:type same_sense: bool, optional
:param uvw_axes: Choose from "UAxes", "VAxes" or "WAxes" depending on
which set of axes the new axis you are adding should belong to.
Defaults to "UAxes".
:type uvw_axes: str, optional
:param grid: The IfcGrid you are adding the axis to.
:type grid: ifcopenshell.entity_instance.entity_instance
:return: The newly created IfcGridAxis
:rtype: ifcopenshell.entity_instance.entity_instance
:param axis_tag: The name of the axis, that would typically be labeled
on drawings or described on site during coordination, such as A, B,
C, 1, 2, 3, etc. Defaults to "A".
:type axis_tag: str, optional
:param same_sense: Determines whether the direction of the axis's line
is reversed. True means the direction the geometry is defined in
represents the direction of the axis. False means the direction is
reversed. Leave as True if unsure. Defaults to "True".
:type same_sense: bool, optional
:param uvw_axes: Choose from "UAxes", "VAxes" or "WAxes" depending on
which set of axes the new axis you are adding should belong to.
Defaults to "UAxes".
:type uvw_axes: str, optional
:param grid: The IfcGrid you are adding the axis to.
:type grid: ifcopenshell.entity_instance
:return: The newly created IfcGridAxis
:rtype: ifcopenshell.entity_instance
Example:
Example:
# A pretty standard rectangular grid, with only two axes.
grid = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcGrid")
axis_a = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="A", uvw_axes="UAxes", grid=grid)
axis_1 = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="1", uvw_axes="VAxes", grid=grid)
"""
self.file = file
self.settings = {
"axis_tag": axis_tag or "A",
"same_sense": same_sense or True,
"uvw_axes": uvw_axes or "UAxes", # Choose which axes
"grid": grid,
}
# A pretty standard rectangular grid, with only two axes.
grid = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcGrid")
axis_a = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="A", uvw_axes="UAxes", grid=grid)
axis_1 = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="1", uvw_axes="VAxes", grid=grid)
"""
def execute(self):
element = self.file.create_entity(
"IfcGridAxis", **{"AxisTag": self.settings["axis_tag"], "SameSense": self.settings["same_sense"]}
)
axes = list(getattr(self.settings["grid"], self.settings["uvw_axes"]) or [])
axes.append(element)
setattr(self.settings["grid"], self.settings["uvw_axes"], axes)
return element
element = file.create_entity("IfcGridAxis", **{"AxisTag": axis_tag, "SameSense": same_sense})
axes = list(getattr(grid, uvw_axes) or [])
axes.append(element)
setattr(grid, uvw_axes, axes)
return element
@@ -19,36 +19,32 @@
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, axis=None):
"""Removes a grid axis from a grid
def remove_grid_axis(file: ifcopenshell.file, axis: ifcopenshell.entity_instance) -> None:
"""Removes a grid axis from a grid
:param axis: The IfcGridAxis you want to remove.
:type axis: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
:param axis: The IfcGridAxis you want to remove.
:type axis: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
Example:
# A pretty standard rectangular grid, with only two axes.
grid = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcGrid")
axis_a = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="A", uvw_axes="UAxes", grid=grid)
axis_1 = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="1", uvw_axes="VAxes", grid=grid)
# A pretty standard rectangular grid, with only two axes.
grid = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcGrid")
axis_a = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="A", uvw_axes="UAxes", grid=grid)
axis_1 = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="1", uvw_axes="VAxes", grid=grid)
# Let's create a third so we can remove it later
axis_2 = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="2", uvw_axes="VAxes", grid=grid)
# Let's create a third so we can remove it later
axis_2 = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="2", uvw_axes="VAxes", grid=grid)
# Let's remove it!
ifcopenshell.api.run("grid.remove_grid_axis", model, axis=axis_2)
"""
self.file = file
self.settings = {"axis": axis}
def execute(self):
if len(self.file.get_inverse(self.settings["axis"].AxisCurve)) == 1:
ifcopenshell.util.element.remove_deep(self.file, self.settings["axis"].AxisCurve)
self.file.remove(self.settings["axis"].AxisCurve)
self.file.remove(self.settings["axis"])
# Let's remove it!
ifcopenshell.api.run("grid.remove_grid_axis", model, axis=axis_2)
"""
axis_curve = axis.AxisCurve
if len(file.get_inverse(axis_curve)) == 1:
ifcopenshell.util.element.remove_deep(file, axis_curve)
file.remove(axis_curve)
file.remove(axis)

Some files were not shown because too many files have changed in this diff Show More