Merge branch 'v0.8.0' into tfk-unify-variant-storage

This commit is contained in:
Thomas Krijnen
2024-08-21 11:24:56 +02:00
422 changed files with 7643 additions and 5015 deletions
@@ -17,14 +17,15 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Optional, Literal
import ifcopenshell.util.representation
from typing import Optional
def add_context(
file: ifcopenshell.file,
context_type: Optional[Literal["Model", "Plan"]] = None,
context_identifier: Optional[str] = None,
target_view: Optional[str] = None,
context_type: Optional[ifcopenshell.util.representation.CONTEXT_TYPE] = None,
context_identifier: Optional[ifcopenshell.util.representation.REPRESENTATION_IDENTIFIER] = None,
target_view: Optional[ifcopenshell.util.representation.TARGET_VIEW] = None,
parent: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance:
"""Adds a new geometric representation context
@@ -106,7 +107,6 @@ def add_context(
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.
@@ -74,7 +74,5 @@ def add_cost_item(
},
)
elif settings["cost_item"]:
ifcopenshell.api.nest.assign_object(
file, related_objects=[cost_item], relating_object=settings["cost_item"]
)
ifcopenshell.api.nest.assign_object(file, related_objects=[cost_item], relating_object=settings["cost_item"])
return cost_item
@@ -26,6 +26,7 @@ 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:
@@ -33,6 +34,7 @@ except ModuleNotFoundError as 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:
@@ -44,6 +46,7 @@ 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:
@@ -857,13 +857,19 @@ 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: list[bpy.types.MeshVertex], is_2d=False, is_model_coords=True):
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])
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])
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])
@@ -33,7 +33,7 @@ def edit_georeferencing(
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
https://docs.blenderbim.org/users/advanced/georeferencing.html
For more information about the attributes and data types of an
IfcCoordinateOperation, consult the IFC documentation.
@@ -57,7 +57,7 @@ def edit_true_north(file: ifcopenshell.file, true_north: Optional[Union[tuple[fl
for context in file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.TrueNorth and true_north is None:
old_true_north = context.TrueNorth
old_true_north = context.TrueNorth
context.TrueNorth = None
if not file.get_total_inverses(old_true_north):
ifcopenshell.util.element.remove_deep2(file, old_true_north)
@@ -22,6 +22,7 @@ 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:
@@ -20,7 +20,10 @@ from typing import Optional
def add_material(
file: ifcopenshell.file, name: Optional[str] = None, category: Optional[str] = None, description: Optional[str] = None
file: ifcopenshell.file,
name: Optional[str] = None,
category: Optional[str] = None,
description: Optional[str] = None,
) -> ifcopenshell.entity_instance:
"""Adds a new material
@@ -50,8 +53,8 @@ def add_material(
Note that categories are not available in IFC2X3. This shortcoming is
one of the big reasons projects should upgrade to IFC4.
Additionally, a material's description provides more information beyond
its name or category.
Additionally, a material's description provides more information beyond
its name or category.
:param name: The name of the material, typically tagged in a finishes
drawing or schedule.
@@ -78,7 +81,7 @@ def add_material(
# "Style" has been specified.
ifcopenshell.api.material.assign_material(model, products=[concrete_bench], material=concrete)
"""
settings = {"name": name or "Unnamed", "category": category, "description": description }
settings = {"name": name or "Unnamed", "category": category, "description": description}
material = file.create_entity("IfcMaterial", **{"Name": settings["name"] or "Unnamed"})
if settings["category"]:
@@ -74,6 +74,9 @@ def assign_material(
:param material: The IfcMaterial or material set you are assigning here.
If type is Usage then no need to provide `material`, it will be deduced
from the element type automatically.
If IfcMaterial is provided as material and type is not IfcMaterial,
provided material will be ignored except for IfcMaterialList
where it will be used as part of the list.
:type material: ifcopenshell.entity_instance, optional
:return: IfcRelAssociatesMaterial entity
or a list of IfcRelAssociatesMaterial entities
@@ -97,6 +97,7 @@ def assign_profile(
class Usecase:
file: ifcopenshell.file
def execute(self) -> None:
# TODO: handle composite profiles
old_profile = self.settings["material_profile"].Profile
@@ -19,7 +19,9 @@ import ifcopenshell
from typing import Any
def edit_assigned_material(file: ifcopenshell.file, element: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
def edit_assigned_material(
file: ifcopenshell.file, element: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcMaterial
For more information about the attributes and data types of an
@@ -18,7 +18,9 @@
import ifcopenshell
def add_role(file: ifcopenshell.file, assigned_object: ifcopenshell.entity_instance, role: str = "ARCHITECT") -> ifcopenshell.entity_instance:
def add_role(
file: ifcopenshell.file, assigned_object: ifcopenshell.entity_instance, role: str = "ARCHITECT"
) -> ifcopenshell.entity_instance:
"""Adds and assigns a new role
People and organisations must play one or more roles on a project. Roles
@@ -19,7 +19,9 @@ import ifcopenshell
from typing import Any
def edit_organisation(file: ifcopenshell.file, organisation: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
def edit_organisation(
file: ifcopenshell.file, organisation: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcOrganization
For more information about the attributes and data types of an
@@ -95,7 +95,7 @@ def add_pset(file: ifcopenshell.file, product: ifcopenshell.entity_instance, nam
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
"Name": settings["name"],
}
},
)
file.create_entity(
"IfcRelDefinesByProperties",
@@ -104,7 +104,7 @@ def add_pset(file: ifcopenshell.file, product: ifcopenshell.entity_instance, nam
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
"RelatedObjects": [settings["product"]],
"RelatingPropertyDefinition": pset,
}
},
)
return pset
elif settings["product"].is_a("IfcTypeObject"):
@@ -118,7 +118,7 @@ def add_pset(file: ifcopenshell.file, product: ifcopenshell.entity_instance, nam
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
"Name": settings["name"],
}
},
)
has_property_sets = list(settings["product"].HasPropertySets or [])
has_property_sets.append(pset)
@@ -142,7 +142,7 @@ def add_pset(file: ifcopenshell.file, product: ifcopenshell.entity_instance, nam
**{
"Name": settings["name"],
"Material": settings["product"],
}
},
)
elif settings["product"].is_a("IfcProfileDef"):
# in IFC2X3 IfcProfileProperties doesn't have Name and we cannot identify them
@@ -50,7 +50,9 @@ def edit_prop_template(
if enum_values := attributes.get("Enumerators", None):
prop_name = attributes.get("Name", None) or getattr(prop_template, "Name", None) or "Unnamed"
primary_measure_type = (
attributes.get("PrimaryMeasureType", None) or getattr(prop_template, "PrimaryMeasureType", None) or "IfcLabel"
attributes.get("PrimaryMeasureType", None)
or getattr(prop_template, "PrimaryMeasureType", None)
or "IfcLabel"
)
enum_values = [file.create_entity(primary_measure_type, v) for v in enum_values]
if enumerators := prop_template.Enumerators:
@@ -38,10 +38,7 @@ def add_structural_load_case(
"""
load_case = ifcopenshell.api.root.create_entity(
file,
ifc_class="IfcStructuralLoadCase",
predefined_type="LOAD_CASE",
name=name
file, ifc_class="IfcStructuralLoadCase", predefined_type="LOAD_CASE", name=name
)
load_case.ActionType = action_type
load_case.ActionSource = action_source
@@ -83,11 +83,15 @@ def register_schema_attributes(schema: ifcopenshell_wrapper.schema_definition) -
# resolve to actual functions in wrapper
functions = [
set_derived_attribute
if mname == "setArgumentAsDerived"
else set_unsupported_attribute
if mname == "setArgumentAsUnknown"
else getattr(ifcopenshell_wrapper.entity_instance, mname)
(
set_derived_attribute
if mname == "setArgumentAsDerived"
else (
set_unsupported_attribute
if mname == "setArgumentAsUnknown"
else getattr(ifcopenshell_wrapper.entity_instance, mname)
)
)
for mname in fn_names
]
@@ -203,15 +207,20 @@ class entity_instance:
rules = importlib.import_module(f"ifcopenshell.express.rules.{schema_name}")
except:
import os
current_dir_files = {fn.lower(): fn for fn in os.listdir('.')}
exp_filename = schema_name.lower() + '.exp'
current_dir_files = {fn.lower(): fn for fn in os.listdir(".")}
exp_filename = schema_name.lower() + ".exp"
schema_path = current_dir_files.get(exp_filename)
if schema_path is None:
raise Exception(f"Couldn't find express file '{schema_name.lower()}.exp' in the current folder: '{os.getcwd()}'.")
fn = schema_path[:-4] + '.py'
raise Exception(
f"Couldn't find express file '{schema_name.lower()}.exp' in the current folder: '{os.getcwd()}'."
)
fn = schema_path[:-4] + ".py"
if not os.path.exists(fn):
subprocess.run([sys.executable, "-m", "ifcopenshell.express.rule_compiler", schema_path, fn], check=True)
time.sleep(1.)
subprocess.run(
[sys.executable, "-m", "ifcopenshell.express.rule_compiler", schema_path, fn], check=True
)
time.sleep(1.0)
rules = importlib.import_module(schema_name)
def yield_supertypes():
@@ -255,7 +264,7 @@ class entity_instance:
# Define condition and transformation functions
condition = lambda v: v == old
transform = lambda v: new
# Usage example
attribute_value = element.RelatedElements
print(old in attribute_value, new in attribute_value) # True, False
+7 -7
View File
@@ -24,7 +24,7 @@ import zipfile
import functools
import ifcopenshell
from pathlib import Path
from typing import Optional, Any, Union, Callable, Generator
from typing import Optional, Any, Union, Callable, Generator, Literal
from . import ifcopenshell_wrapper
from .entity_instance import entity_instance
@@ -382,10 +382,10 @@ class file:
for (attr_index, _), attr_name in zip(kwargs_attrs, kwargs):
if attr_index == 0xFFFFFFFF:
invalid_attrs.append(attr_name)
raise ValueError(
"entity instance of type '%s' doesn't have the following attributes: %s."
% (e.is_a(True), ", ".join(invalid_attrs))
)
raise ValueError(
"entity instance of type '%s' doesn't have the following attributes: %s."
% (e.is_a(True), ", ".join(invalid_attrs))
)
# Restore transaction status
if attrs:
@@ -406,7 +406,7 @@ class file:
return e
@property
def schema(self) -> str:
def schema(self) -> Literal["IFC2X3", "IFC4", "IFC4X3"]:
"""General IFC schema version: IFC2X3, IFC4, IFC4X3."""
prefixes = ("IFC", "X", "_ADD", "_TC")
reg = "".join(f"(?P<{s}>{s}\d+)?" for s in prefixes)
@@ -668,5 +668,5 @@ class file:
return file(ifcopenshell_wrapper.read(s))
@staticmethod
def from_pointer(v):
def from_pointer(v) -> "file":
return file_dict.get(v)()
@@ -204,7 +204,6 @@ class application(QtWidgets.QApplication):
with two tree views and a graphical 3d view"""
class abstract_treeview(QtWidgets.QTreeWidget):
"""Base class for the two treeview controls"""
instanceSelected = QtCore.pyqtSignal([object])
@@ -255,7 +254,6 @@ class application(QtWidgets.QApplication):
)
class decomposition_treeview(abstract_treeview):
"""Treeview with typical IFC decomposition relationships"""
ATTRIBUTES = ["Entity", "GlobalId", "Name"]
@@ -301,7 +299,6 @@ class application(QtWidgets.QApplication):
self.expandAll()
class type_treeview(abstract_treeview):
"""Treeview with typical IFC decomposition relationships"""
ATTRIBUTES = ["Name"]
@@ -33,6 +33,8 @@ from typing import TypeVar, Union, Optional, Generator, Any, Literal, overload,
if TYPE_CHECKING:
from OCC.Core import TopoDS
IteratorOutput = Union["ShapeElementType", "utils.shape_tuple"]
T = TypeVar("T")
ShapeElementType = Union[
ifcopenshell_wrapper.BRepElement, ifcopenshell_wrapper.TriangulationElement, ifcopenshell_wrapper.SerializedElement
@@ -273,7 +275,7 @@ class iterator(ifcopenshell_wrapper.Iterator):
def get(self):
return wrap_shape_creation(self.settings, ifcopenshell_wrapper.Iterator.get(self))
def __iter__(self) -> Generator[ShapeElementType, None, None]:
def __iter__(self) -> Generator[IteratorOutput, None, None]:
if self.initialize():
while True:
yield self.get()
@@ -412,7 +414,19 @@ def create_shape(
)
def consume_iterator(it, with_progress=False):
@overload
def consume_iterator(it: iterator, with_progress: Literal[False] = False) -> Generator[IteratorOutput, None, None]: ...
@overload
def consume_iterator(
it: iterator, with_progress: Literal[True]
) -> Generator[tuple[int, IteratorOutput], None, None]: ...
@overload
def consume_iterator(
it: iterator, with_progress: bool
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]: ...
def consume_iterator(
it: iterator, with_progress: bool = False
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]:
if it.initialize():
while True:
if with_progress:
@@ -423,16 +437,49 @@ def consume_iterator(it, with_progress=False):
break
@overload
def iterate(
settings,
file_or_filename,
num_threads=1,
include=None,
exclude=None,
with_progress=False,
cache=None,
settings: settings,
file_or_filename: Union[file, str],
num_threads: int = 1,
include: Optional[Union[list[entity_instance], list[str]]] = None,
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
with_progress: Literal[False] = False,
cache: Optional[serializers.hdf5] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
):
) -> Generator[IteratorOutput, None, None]: ...
@overload
def iterate(
settings: settings,
file_or_filename: Union[file, str],
num_threads: int = 1,
include: Optional[Union[list[entity_instance], list[str]]] = None,
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
with_progress: Literal[True] = True,
cache: Optional[serializers.hdf5] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[tuple[int, IteratorOutput], None, None]: ...
@overload
def iterate(
settings: settings,
file_or_filename: Union[file, str],
num_threads: int = 1,
include: Optional[Union[list[entity_instance], list[str]]] = None,
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
with_progress: bool = False,
cache: Optional[serializers.hdf5] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]: ...
def iterate(
settings: settings,
file_or_filename: Union[file, str],
num_threads: int = 1,
include: Optional[Union[list[entity_instance], list[str]]] = None,
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
with_progress: bool = False,
cache: Optional[serializers.hdf5] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]:
it = iterator(settings, file_or_filename, num_threads, include, exclude, geometry_library)
if cache:
hdf5_cache = serializers.hdf5(cache, settings)
@@ -40,6 +40,8 @@ except ImportError:
class shape_tuple(NamedTuple):
"""A tuple containing IfcOpenShell serialized element/shape and pythonOCC shape."""
data: Union[ifcopenshell_wrapper.SerializedElement, ifcopenshell_wrapper.Serialization]
geometry: TopoDS.TopoDS_Shape
styles: tuple[tuple[float, float, float, float], ...]
+1 -1
View File
@@ -37,7 +37,7 @@ def compress(g):
bs = [int(g[i : i + 2], 16) for i in range(0, len(g), 2)]
def b64(v, l=4):
return "".join([chars[(v // (64 ** i)) % 64] for i in range(l)][::-1])
return "".join([chars[(v // (64**i)) % 64] for i in range(l)][::-1])
return "".join([b64(bs[0], 2)] + [b64((bs[i] << 16) + (bs[i + 1] << 8) + bs[i + 2]) for i in range(1, 16, 3)])
@@ -33,4 +33,4 @@ element or None. Example:
#2=IfcRelAssignsToGroup($,$,$,$,$,$,#1)
"""
unpack_non_aggregate_inverses = False
unpack_non_aggregate_inverses = False
+62 -64
View File
@@ -11,19 +11,19 @@ try:
class StreamTransformer(Transformer):
def string(self, items):
return str(items[0])[1:-1]
def float(self, items):
return float(items[0])
def ifcint(self, items):
return int(items[0])
def null(self, items):
return None
def derived(self, items):
return None
def enum(self, items):
if items[0] == ".T.":
return True
@@ -32,7 +32,7 @@ try:
elif items[0] == ".U.":
return "UNKNOWN"
return str(items[0])[1:-1]
def list(self, items):
# List is always called twice, I think due to an ambiguity in the Lark
# definition between a list and an arg, but I'm not quite sure.
@@ -40,7 +40,7 @@ try:
if items and isinstance(items[0], dict):
return tuple(items[0]["list"])
return {"list": items}
def inline_type(self, items):
# inline_type is also always called twice. Why?
if items and isinstance(items[0], dict):
@@ -48,20 +48,19 @@ try:
entity = ifcopenshell.create_entity(items[0])
entity[0] = items[1]
return {"inline_type": entity}
def reference(self, items):
return self.file.by_id(int(items[0][1:]))
def arg(self, items):
return items[0]
def args(self, items):
return items
def start(self, items):
return (int(items[0]), str(items[1]), items[2])
class stream(file):
def __init__(self, filepath):
self.wrapped_data = None
@@ -69,9 +68,9 @@ try:
self.history = []
self.future = []
self.transaction = None
self.filepath = filepath
self.file = open(filepath, "r")
self.id_map = {}
self.class_map = {}
@@ -80,7 +79,7 @@ try:
self.reference_pattern = re.compile(r"#(\d+)")
self.entity_cache = {}
self.inverses = {}
# common.INT doesn't support negative integers.
grammar = r"""
start: "#" NUMBER "=" TYPE "(" args ")" ";"
@@ -114,11 +113,11 @@ try:
%import common.INT
%import common.CNAME
"""
transformer = StreamTransformer()
transformer.file = self
self.parser = Lark(grammar, parser="lalr", transformer=transformer)
exclude_classes = [
"IfcObjectPlacement",
"IfcPresentationItem",
@@ -128,9 +127,9 @@ try:
"IfcRepresentationItem",
]
exclude_classes = []
exclude = set()
offset = 0
for line in self.file:
line = line.strip()
@@ -138,14 +137,14 @@ try:
step_id, ifc_class = line.split("(")[0].split("=")
step_id = int(step_id.strip()[1:])
ifc_class = ifc_class.strip()
if ifc_class in exclude:
offset += len(line) + 1 # +1 for the newline character
continue
for reference_id in self.reference_pattern.findall(line[1:]):
self.inverses.setdefault(int(reference_id), []).append(step_id)
self.id_map[step_id] = ifc_class
self.class_map.setdefault(ifc_class, []).append(step_id)
self.id_offset[step_id] = offset
@@ -156,9 +155,9 @@ try:
declaration = self.ifc_schema.declaration_by_name(ifc_class)
exclude.update([st.name().upper() for st in ifcopenshell.util.schema.get_subtypes(declaration)])
offset += len(line) + 1 # +1 for the newline character
self.preprocess_schema()
def preprocess_schema(self):
self.ifc_class_names = {}
self.ifc_class_subtypes = {}
@@ -166,32 +165,32 @@ try:
self.ifc_class_inverse_attributes = {}
self.ifc_class_references = {}
self.ifc_class_inverses = {}
for declaration in self.ifc_schema.entities():
self.ifc_class_names[declaration.name().upper()] = declaration.name()
self.ifc_class_subtypes[declaration.name()] = ifcopenshell.util.schema.get_subtypes(declaration)
self.ifc_class_attributes[declaration.name()] = {a.name(): a for a in declaration.all_attributes()}
self.ifc_class_inverse_attributes[declaration.name()] = {
a.name(): a for a in declaration.all_inverse_attributes()
}
entity = []
entity_list = []
for attribute in declaration.all_attributes():
primitive = ifcopenshell.util.attribute.get_primitive_type(attribute)
if primitive == "entity":
entity.append(attribute.name())
attribute_entity = attribute.type_of_attribute().declared_type()
for subtype in ifcopenshell.util.schema.get_subtypes(attribute_entity):
self.ifc_class_inverses.setdefault(subtype.name(), {})
self.ifc_class_inverses[subtype.name()].setdefault(declaration.name(), [])
self.ifc_class_inverses[subtype.name()][declaration.name()].append(attribute.name())
elif self.is_entity_list(attribute):
entity_list.append(attribute.name())
for entity_name in re.findall("<entity (.*?)>", str(attribute)):
attribute_entity = self.ifc_schema.declaration_by_name(entity_name)
for subtype in ifcopenshell.util.schema.get_subtypes(attribute_entity):
@@ -199,15 +198,15 @@ try:
self.ifc_class_inverses.setdefault(subtype.name(), {})
self.ifc_class_inverses[subtype.name()].setdefault(declaration.name(), [])
self.ifc_class_inverses[subtype.name()][declaration.name()].append(attribute.name())
self.ifc_class_references[declaration.name()] = {"entity": entity, "entity_list": entity_list}
def clear_cache(self):
self.entity_cache = {}
def create_entity(self, type, *args, **kawrgs):
assert False
def by_id(self, id):
entity = self.entity_cache.get(id, None)
if entity:
@@ -217,35 +216,35 @@ try:
entity = stream_entity(id, self.ifc_class_names[ifc_class], self)
self.entity_cache[id] = entity
return entity
def by_type(self, type, include_subtypes=True):
results = []
subtypes = self.ifc_class_subtypes[type] if include_subtypes else self.ifc_class_subtypes[type][0:1]
for subtype in subtypes:
results.extend([self.by_id(i) for i in self.class_map.get(subtype.name().upper(), [])])
return results
def traverse(self, inst, max_levels=None, breadth_first=False):
results = [inst]
queue = [inst]
while queue:
if max_levels is not None:
max_levels -= 1
cur = queue.pop()
level_results = set()
for reference_id in self.reference_pattern.findall(str(cur)[1:]):
result = self.by_id(int(reference_id))
results.append(result)
if max_levels is None or max_levels:
queue.append(result)
return results
def get_inverse(self, inst, allow_duplicate=False, with_attribute_indices=False):
return {self.by_id(e) for e in self.inverses.get(inst.stream_wrapper.id, [])}
def is_entity_list(self, attribute):
attribute = str(attribute.type_of_attribute())
if (attribute.startswith("<list") or attribute.startswith("<set")) and "<entity" in attribute:
@@ -254,8 +253,7 @@ try:
return False
return True
return False
class stream_entity(entity_instance):
def __init__(self, id, ifc_class, file=None):
if not ifc_class:
@@ -265,39 +263,39 @@ try:
s = stream_wrapper(id, ifc_class, file)
super(entity_instance, self).__setattr__("wrapped_data", e)
super(entity_instance, self).__setattr__("stream_wrapper", s)
def id(self):
return self.stream_wrapper.id
def __repr__(self):
offset = self.stream_wrapper.file.id_offset[self.stream_wrapper.id]
self.stream_wrapper.file.file.seek(offset)
return self.stream_wrapper.file.file.readline().strip()
def __del__(self):
pass
def __getitem__(self, key):
return self.__getattr__(list(self.stream_wrapper.attributes.keys())[key])
def __setattr__(self, key, value):
query = f"UPDATE `{self.stream_wrapper.ifc_class}` SET `{key}` = ? WHERE ifc_id = {self.stream_wrapper.id}"
self.stream_wrapper.file.cursor.execute(query, (value,))
self.stream_wrapper.file.db.commit()
self.stream_wrapper.attribute_cache = {}
def __getattr__(self, name):
INVALID, FORWARD, INVERSE = range(3)
attr_cat = self.wrapped_data.get_attribute_category(name)
if attr_cat == FORWARD:
if self.stream_wrapper.attribute_cache:
return self.stream_wrapper.attribute_cache[name]
offset = self.stream_wrapper.file.id_offset[self.stream_wrapper.id]
self.stream_wrapper.file.file.seek(offset)
line = self.stream_wrapper.file.file.readline()
attributes = self.stream_wrapper.file.parser.parse(line.strip())[2]
for i, attribute in enumerate(self.stream_wrapper.attributes.values()):
self.stream_wrapper.attribute_cache[attribute.name()] = attributes[i]
return self.stream_wrapper.attribute_cache[name]
@@ -306,19 +304,19 @@ try:
results = self.stream_wrapper.inverse_attribute_cache.get(name, None)
if results is not None:
return results
results = []
element_ids = self.stream_wrapper.file.inverses.get(self.stream_wrapper.id, [])
if not element_ids:
self.stream_wrapper.inverse_attribute_cache[name] = tuple()
return self.stream_wrapper.inverse_attribute_cache[name]
attribute = self.stream_wrapper.inverse_attributes[name]
entity_class = attribute.entity_reference().name()
declaration = self.stream_wrapper.file.ifc_schema.declaration_by_name(entity_class)
forward_name = attribute.attribute_reference().name()
subtypes = [st.name() for st in ifcopenshell.util.schema.get_subtypes(declaration)]
for element_id in element_ids:
ifc_class = self.stream_wrapper.file.ifc_class_names[self.stream_wrapper.file.id_map[element_id]]
@@ -332,14 +330,14 @@ try:
results.append(potential_result)
elif forward_value.id() == self.stream_wrapper.id:
results.append(potential_result)
self.stream_wrapper.inverse_attribute_cache[name] = tuple(results)
return self.stream_wrapper.inverse_attribute_cache[name]
raise AttributeError(
"entity instance of type '%s' has no attribute '%s'" % (self.wrapped_data.is_a(True), name)
)
def __eq__(self, other):
if not isinstance(self, type(other)):
return False
@@ -348,19 +346,18 @@ try:
if self.stream_wrapper.id:
return self.stream_wrapper.id == other.stream_wrapper.id
assert False # not implemented
def __hash__(self):
if self.stream_wrapper.id:
return hash((self.stream_wrapper.id, self.stream_wrapper.file.filepath))
def get_info(self, include_identifier=True, recursive=False, return_type=dict, ignore=(), scalar_only=False):
info = {"id": self.stream_wrapper.id, "type": self.stream_wrapper.ifc_class}
if not self.stream_wrapper.attribute_cache:
self.__getitem__(0) # This will get all attributes
info.update(self.stream_wrapper.attribute_cache)
return info
class stream_wrapper:
def __init__(self, id, ifc_class, file):
self.id = id
@@ -370,10 +367,11 @@ try:
self.inverse_attributes = self.file.ifc_class_inverse_attributes[self.ifc_class]
self.attribute_cache = {}
self.inverse_attribute_cache = {}
def __repr__(self):
return "todo"
except ImportError as e:
import sys
print(f"No stream support: {e}", file=sys.stderr)
@@ -60,14 +60,14 @@ class TransitionCurve:
def _calc_biquadratic_parabola_point(self, lpt, L, R, ccw):
x = lpt
if x <= (L / 2):
y = x ** 4 / (6 * R * L ** 2)
y = x**4 / (6 * R * L**2)
else:
yterm_1 = (-1 * x ** 4) / (6 * R * L ** 2)
yterm_2 = (2 * x ** 3) / (3 * R * L)
yterm_3 = x ** 2 / (2 * R)
yterm_1 = (-1 * x**4) / (6 * R * L**2)
yterm_2 = (2 * x**3) / (3 * R * L)
yterm_3 = x**2 / (2 * R)
yterm_4 = (L * x) / (6 * R)
yterm_5 = L ** 2 / (48 * R)
yterm_5 = L**2 / (48 * R)
y = yterm_1 + yterm_2 - yterm_3 + yterm_4 - yterm_5
@@ -82,16 +82,16 @@ class TransitionCurve:
def _calc_clothoid_curve_point(self, lpt, L, R, ccw):
RL = R * L
xterm_1 = 1
xterm_2 = lpt ** 4 / (40 * RL ** 2)
xterm_3 = lpt ** 8 / (3456 * RL ** 4)
xterm_4 = lpt ** 12 / (599040 * RL ** 6)
xterm_2 = lpt**4 / (40 * RL**2)
xterm_3 = lpt**8 / (3456 * RL**4)
xterm_4 = lpt**12 / (599040 * RL**6)
x = lpt * (xterm_1 - xterm_2 + xterm_3 - xterm_4)
factor = lpt ** 3 / (6 * RL)
factor = lpt**3 / (6 * RL)
yterm_1 = 1
yterm_2 = lpt ** 4 / (56 * RL ** 2)
yterm_3 = lpt ** 8 / (7040 * RL ** 4)
yterm_4 = lpt ** 12 / (1612800 * RL ** 6)
yterm_2 = lpt**4 / (56 * RL**2)
yterm_3 = lpt**8 / (7040 * RL**4)
yterm_4 = lpt**12 / (1612800 * RL**6)
y = factor * (yterm_1 - yterm_2 + yterm_3 - yterm_4)
@@ -104,9 +104,9 @@ class TransitionCurve:
pi = math.pi
psi_x = (pi * lpt) / L
xterm_1 = (L ** 2) / (8.0 * pi ** 2 * R ** 2)
xterm_1 = (L**2) / (8.0 * pi**2 * R**2)
xterm_2 = L / pi
xterm_3 = psi_x ** 3 / (3.0)
xterm_3 = psi_x**3 / (3.0)
xterm_4 = psi_x / (2.0)
xterm_5 = (math.sin(psi_x) * math.cos(psi_x)) / (2.0)
xterm_6 = psi_x * math.cos(psi_x)
@@ -130,6 +130,8 @@ def get_attributes_keep_md(resource, builder):
# Temporary fix for https://github.com/buildingSMART/IFC4.3.x-development/issues/754.
_original_get_resource_path = get_resource_path
def get_resource_path(resource: str, abort_on_error=False) -> Union[str, None]:
md = _original_get_resource_path(resource, abort_on_error)
if md and resource == "IfcURIReference":
@@ -74,8 +74,7 @@ def get_calendar(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entit
calendar = [
rel.RelatingControl
for rel in task.HasAssignments or []
if rel.is_a("IfcRelAssignsToControl")
and rel.RelatingControl.is_a("IfcWorkCalendar")
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkCalendar")
]
if calendar:
return calendar[0]
@@ -88,11 +87,7 @@ def count_working_days(start, finish, calendar: ifcopenshell.entity_instance) ->
current_date = datetime.date(start.year, start.month, start.day)
finish_date = datetime.date(finish.year, finish.month, finish.day)
while current_date <= finish_date:
if (
calendar
and calendar.WorkingTimes
and is_working_day(current_date, calendar)
):
if calendar and calendar.WorkingTimes and is_working_day(current_date, calendar):
result += 1
elif not calendar or not is_calendar_applicable(current_date, calendar):
result += 1
@@ -132,9 +127,7 @@ def offset_date(start, duration, duration_type: DURATION_TYPE, calendar: ifcopen
abs_duration = abs((duration.days + months * 30 + years * 12 * 30))
date_offset = datetime.timedelta(days=1 if duration.days > 0 else -1)
while abs_duration > 0:
if duration_type == "ELAPSEDTIME" or not is_calendar_applicable(
current_date, calendar
):
if duration_type == "ELAPSEDTIME" or not is_calendar_applicable(current_date, calendar):
abs_duration -= 1
elif is_working_day(current_date, calendar):
abs_duration -= 1
@@ -245,16 +238,13 @@ def is_work_time_applicable_to_day(work_time: ifcopenshell.entity_instance, day)
return False # TODO
elif recurrence_type == "MONTHLY_BY_POSITION":
if not recurrence.Interval and not recurrence.Occurrences:
return (day.weekday() + 1) in recurrence.WeekdayComponent and floor(
day.day / 7
) + 1 == recurrence["Position"]
return (day.weekday() + 1) in recurrence.WeekdayComponent and floor(day.day / 7) + 1 == recurrence[
"Position"
]
return False # TODO
elif recurrence_type == "YEARLY_BY_DAY_OF_MONTH":
if not recurrence.Interval and not recurrence.Occurrences:
return (
day.month in recurrence.MonthComponent
and day.day in recurrence.DayComponent
)
return day.month in recurrence.MonthComponent and day.day in recurrence.DayComponent
return False # TODO
elif recurrence_type == "YEARLY_BY_POSITION":
if not recurrence.Interval and not recurrence.Occurrences:
@@ -272,9 +262,7 @@ def get_task_work_schedule(task: ifcopenshell.entity_instance) -> Union[ifcopens
return get_task_work_schedule(parent_task) or get_task_work_schedule(task)
else:
for rel in task.HasAssignments:
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a(
"IfcWorkSchedule"
):
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkSchedule"):
return rel.RelatingControl
return None
@@ -304,21 +292,11 @@ def get_work_schedule_tasks(work_schedule: ifcopenshell.entity_instance) -> list
def get_root_tasks(work_schedule: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return [
obj
for rel in work_schedule.Controls
for obj in rel.RelatedObjects
if obj.is_a("IfcTask")
]
return [obj for rel in work_schedule.Controls for obj in rel.RelatedObjects if obj.is_a("IfcTask")]
def get_root_tasks_ids(work_schedule: ifcopenshell.entity_instance) -> list[int]:
return [
obj.id()
for rel in work_schedule.Controls
for obj in rel.RelatedObjects
if obj.is_a("IfcTask")
]
return [obj.id() for rel in work_schedule.Controls for obj in rel.RelatedObjects if obj.is_a("IfcTask")]
def guess_date_range(work_schedule: ifcopenshell.entity_instance):
@@ -344,22 +322,14 @@ def guess_date_range(work_schedule: ifcopenshell.entity_instance):
def get_direct_task_outputs(task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return [
rel.RelatingProduct
for rel in task.HasAssignments
if rel.is_a("IfcRelAssignsToProduct")
]
return [rel.RelatingProduct for rel in task.HasAssignments if rel.is_a("IfcRelAssignsToProduct")]
def get_task_outputs(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]:
if not is_deep:
return get_direct_task_outputs(task)
else:
return [
output
for nested_task in get_all_nested_tasks(task)
for output in get_direct_task_outputs(nested_task)
]
return [output for nested_task in get_all_nested_tasks(task) for output in get_direct_task_outputs(nested_task)]
def get_task_inputs(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]:
@@ -434,8 +404,7 @@ def get_tasks_for_product(
inputs = [
assignement.RelatingProcess
for assignement in product.HasAssignments
if assignement.is_a("IfcRelAssignsToProcess")
and assignement.RelatingProcess.is_a("IfcTask")
if assignement.is_a("IfcRelAssignsToProcess") and assignement.RelatingProcess.is_a("IfcTask")
]
outputs = [
obj
@@ -446,16 +415,8 @@ def get_tasks_for_product(
]
if schedule:
inputs = [
task
for task in inputs
if get_task_work_schedule(task).id() == schedule.id()
]
outputs = [
task
for task in outputs
if get_task_work_schedule(task).id() == schedule.id()
]
inputs = [task for task in inputs if get_task_work_schedule(task).id() == schedule.id()]
outputs = [task for task in outputs if get_task_work_schedule(task).id() == schedule.id()]
return inputs, outputs