Merge branch 'v0.7.0' of https://github.com/bdamay/IfcOpenShell into v0.7.0

This commit is contained in:
Benoit DAMAY
2024-03-04 17:11:54 +01:00
11 changed files with 1944 additions and 78 deletions
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1504,7 +1504,7 @@ class Drawing(blenderbim.core.tool.Drawing):
def get_drawing_metadata(cls, drawing):
return [
v.strip()
for v in ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"].get("Metadata", "").split(",")
for v in (ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"].get("Metadata", "") or "").split(",")
if v
]
@@ -16,7 +16,9 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import bpy
import csv
import random
import ifcopenshell
import ifcopenshell.api
@@ -30,6 +32,10 @@ from mathutils import Vector, Matrix
from random import uniform
# When run from Blender
BLEND_DIR = os.path.dirname(bpy.data.filepath)
OUT_PATH = os.path.join(BLEND_DIR, "..", "blenderbim", "bim", "data", "libraries", "IFC4 Landscape Library.ifc")
SimpleTreeParams = namedtuple("SimpleTreeParams", "plant_height crown_diameter trunk_diameter")
LowPolyTreeParams = namedtuple(
"LowPolyTreeParams", "plant_height crown_diameter crown_max_loc crown_taper trunk_height trunk_diameter random_seed"
@@ -301,7 +307,7 @@ tree_presets = {
class LibraryGenerator:
def generate(self, library_name, output_filename="IFC4 EU Steel.ifc"):
def generate(self, library_name, output_filename):
ifcopenshell.api.pre_listeners = {}
ifcopenshell.api.post_listeners = {}
@@ -368,7 +374,7 @@ class LibraryGenerator:
representations[rep_key] = rep_obj.name
self.create_type("IfcGeographicElementType", obj.name, representations)
# Auto generated trees
# Auto generated generic trees
self.builder = ShapeBuilder(self.file)
builder = self.builder
@@ -401,6 +407,21 @@ class LibraryGenerator:
"IfcGeographicElementType", tree_data.tree_name, **self.get_representations(tree_geometry)
)
# From tree species table
with open(os.path.join(BLEND_DIR, "tree_species.csv"), 'r') as csvfile:
reader = csv.reader(csvfile)
for i, row in enumerate(reader):
if i == 0:
continue
preset = tree_presets.get(row[0])
data = [float(x) for x in row[2:]]
preset_data = preset.preset_class(*data)._asdict()
tree_geometry = preset.generator(builder, **preset_data)
self.create_explicit_type(
"IfcGeographicElementType", row[1], **self.get_representations(tree_geometry)
)
self.file.write(output_filename)
def get_representations(self, generated_geometry: GeneratedGeometry):
@@ -464,4 +485,4 @@ class LibraryGenerator:
if __name__ == "__main__":
LibraryGenerator().generate("Landscape Assets Library", output_filename="IFC4 Landscape Library.ifc")
LibraryGenerator().generate("Landscape Assets Library", output_filename=OUT_PATH)
+27
View File
@@ -0,0 +1,27 @@
"f","name","PlantHeight","CrownDiameter","CrownMaxLoc","CrownTaper","TrunkHeight","TrunkDiameter","RandomSeed"
"low_poly","Apple",8,9,0.5,0.2,1.5,0.2,1
"low_poly","Beech",30,20,0,0.4,3,0.75,2
"low_poly","Cedar",25,17,0,1,1,0.625,3
"low_poly","Dogwood",8,6,1,0.9,0.3,0.2,4
"low_poly","Elm",17,12,0.6,0.7,2,0.425,5
"low_poly","Fir",27,15,0.1,1,0.1,0.675,6
"low_poly","Ginkgo",25,9,0.5,0.6,7,0.625,7
"low_poly","Horsechesnut",30,23,0.5,0.2,3,0.75,8
"low_poly","Ilex",26,13,0.4,0.3,12,0.65,9
"low_poly","Juniper",30,15,0.7,1,1,0.75,10
"low_poly","Kentucky Coffeetree",20,10,0.5,0,6,0.5,11
"low_poly","Linden",28,18,0.3,0.2,3,0.7,12
"low_poly","Mulberry",15,12,0.5,0.5,2,0.375,13
"low_poly","Nannybery",5,4,0.7,0.8,1,0.125,14
"low_poly","Oak",25,13,0.5,0.5,6,0.625,15
"low_poly","Pine",27,9,0.5,0.2,14,0.675,16
"low_poly","Quaking Aspen",13,4,0.5,0.3,4,0.325,17
"low_poly","Redwood",90,24,0.1,0.8,8,2.25,18
"low_poly","Sycamore",35,22,0.2,0.6,5,0.875,19
"low_poly","Tree of Heaven",20,15,0.2,0.8,3,0.5,20
"low_poly","Umbrella Tree",8,5,0.4,0.5,2,0.2,21
"low_poly","Viburnum",4,3,0.8,0.7,0.2,0.1,22
"low_poly","Weeping Willow",13,12,0.9,0.3,1,0.325,23
"low_poly","Zanthoxylum",10,8,0.4,0.6,2,0.25,24
"low_poly","Yellowwood",25,20,0.2,0.3,5,0.625,25
"low_poly","Zelkova",12,8,0.4,0.4,2,0.3,26
1 f name PlantHeight CrownDiameter CrownMaxLoc CrownTaper TrunkHeight TrunkDiameter RandomSeed
2 low_poly Apple 8 9 0.5 0.2 1.5 0.2 1
3 low_poly Beech 30 20 0 0.4 3 0.75 2
4 low_poly Cedar 25 17 0 1 1 0.625 3
5 low_poly Dogwood 8 6 1 0.9 0.3 0.2 4
6 low_poly Elm 17 12 0.6 0.7 2 0.425 5
7 low_poly Fir 27 15 0.1 1 0.1 0.675 6
8 low_poly Ginkgo 25 9 0.5 0.6 7 0.625 7
9 low_poly Horsechesnut 30 23 0.5 0.2 3 0.75 8
10 low_poly Ilex 26 13 0.4 0.3 12 0.65 9
11 low_poly Juniper 30 15 0.7 1 1 0.75 10
12 low_poly Kentucky Coffeetree 20 10 0.5 0 6 0.5 11
13 low_poly Linden 28 18 0.3 0.2 3 0.7 12
14 low_poly Mulberry 15 12 0.5 0.5 2 0.375 13
15 low_poly Nannybery 5 4 0.7 0.8 1 0.125 14
16 low_poly Oak 25 13 0.5 0.5 6 0.625 15
17 low_poly Pine 27 9 0.5 0.2 14 0.675 16
18 low_poly Quaking Aspen 13 4 0.5 0.3 4 0.325 17
19 low_poly Redwood 90 24 0.1 0.8 8 2.25 18
20 low_poly Sycamore 35 22 0.2 0.6 5 0.875 19
21 low_poly Tree of Heaven 20 15 0.2 0.8 3 0.5 20
22 low_poly Umbrella Tree 8 5 0.4 0.5 2 0.2 21
23 low_poly Viburnum 4 3 0.8 0.7 0.2 0.1 22
24 low_poly Weeping Willow 13 12 0.9 0.3 1 0.325 23
25 low_poly Zanthoxylum 10 8 0.4 0.6 2 0.25 24
26 low_poly Yellowwood 25 20 0.2 0.3 5 0.625 25
27 low_poly Zelkova 12 8 0.4 0.4 2 0.3 26
@@ -84,12 +84,12 @@ class Usecase:
# Now we can easily append our wall type from our libary
wall_type = ifcopenshell.api.run("project.append_asset", model, library=library, element=wall_type)
"""
self.file = file
self.file: ifcopenshell.file = file
self.settings = {"library": library, "element": element}
def execute(self):
# mapping of old element ids to new elements
self.added_elements:dict[int, ifcopenshell.entity_instance] = {}
self.added_elements: dict[int, ifcopenshell.entity_instance] = {}
self.whitelisted_inverse_attributes = {}
if self.settings["element"].is_a("IfcTypeProduct"):
self.target_class = "IfcTypeProduct"
@@ -268,6 +268,7 @@ class Usecase:
def reuse_existing_contexts(self):
added_contexts = set([e for e in self.added_elements.values() if e.is_a("IfcGeometricRepresentationContext")])
added_contexts -= set(self.existing_contexts)
for added_context in added_contexts:
equivalent_existing_context = self.get_equivalent_existing_context(added_context)
if not equivalent_existing_context:
@@ -30,6 +30,7 @@ import functools
import subprocess
import sys
import time
from typing import Union
from . import ifcopenshell_wrapper
from . import settings
@@ -117,6 +118,8 @@ class entity_instance(object):
>>> #423=IfcProductDefinitionShape($,$,(#409,#421))
"""
wrapped_data: ifcopenshell_wrapper.entity_instance
def __init__(self, e, file=None):
if isinstance(e, tuple):
e = ifcopenshell_wrapper.new_IfcBaseClass(*e)
@@ -221,7 +224,7 @@ class entity_instance(object):
return entity_instance.walk(is_instance, unwrap, v)
def attribute_type(self, attr):
def attribute_type(self, attr: int) -> str:
"""Return the data type of a positional attribute of the element
:param attr: The index of the attribute
@@ -231,7 +234,7 @@ class entity_instance(object):
attr_idx = attr if isinstance(attr, numbers.Integral) else self.wrapped_data.get_argument_index(attr)
return self.wrapped_data.get_argument_type(attr_idx)
def attribute_name(self, attr_idx):
def attribute_name(self, attr_idx: int) -> str:
"""Return the name of a positional attribute of the element
:param attr_idx: The index of the attribute
@@ -272,7 +275,7 @@ class entity_instance(object):
def __repr__(self):
return repr(self.wrapped_data)
def to_string(self, valid_spf=True):
def to_string(self, valid_spf=True) -> str:
"""Returns a string representation of the current entity instance.
Equal to str(self) when valid_spf=False. When valid_spf is True
returns a representation of the string that conforms to valid Step
@@ -283,7 +286,7 @@ class entity_instance(object):
return self.wrapped_data.to_string(valid_spf)
def is_a(self, *args):
def is_a(self, *args) -> Union[str, bool]:
"""Return the IFC class name of an instance, or checks if an instance belongs to a class.
The check will also return true if a parent class name is provided.
@@ -306,7 +309,7 @@ class entity_instance(object):
"""
return self.wrapped_data.is_a(*args)
def id(self):
def id(self) -> int:
"""Return the STEP numerical identifier
:rtype: int
@@ -335,7 +338,7 @@ class entity_instance(object):
other.wrapped_data.file_pointer(),
)
def is_entity(self):
def is_entity(self) -> bool:
"""Tests whether the instance is an entity type as opposed to a simple data type.
Returns:
@@ -430,7 +433,9 @@ class entity_instance(object):
)
)
def get_info(self, include_identifier=True, recursive=False, return_type=dict, ignore=(), scalar_only=False):
def get_info(
self, include_identifier=True, recursive=False, return_type=dict, ignore=(), scalar_only=False
) -> dict:
"""Return a dictionary of the entity_instance's properties (Python and IFC) and their values.
:param include_identifier: Whether or not to include the STEP numerical identifier
+35 -13
View File
@@ -20,6 +20,7 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import annotations
import os
import re
@@ -27,6 +28,7 @@ import numbers
import zipfile
import functools
from pathlib import Path
from typing import Tuple, List
import ifcopenshell.util.element
import ifcopenshell.util.file
@@ -195,7 +197,9 @@ class file(object):
print(products[0] == ifc_file[122] == ifc_file["2XQ$n5SLP5MBLyL442paFx"]) # True
"""
def __init__(self, f=None, schema=None, schema_version=None):
wrapped_data: ifcopenshell_wrapper.file
def __init__(self, f: ifcopenshell_wrapper.file = None, schema: str = None, schema_version: Tuple[int] = None):
"""Create a new blank IFC model
This IFC model does not have any entities in it yet. See the
@@ -253,8 +257,9 @@ class file(object):
self.transaction = None
import weakref
file_dict[self.file_pointer()] = weakref.ref(self)
def __del__(self):
del file_dict[self.file_pointer()]
@@ -294,7 +299,7 @@ class file(object):
transaction.commit()
self.history.append(transaction)
def create_entity(self, type, *args, **kwargs):
def create_entity(self, type: str, *args, **kwargs) -> ifcopenshell.entity_instance:
"""Create a new IFC entity in the file.
:param type: Case insensitive name of the IFC class
@@ -392,30 +397,43 @@ class file(object):
elif isinstance(key, basestring):
return entity_instance(self.wrapped_data.by_guid(str(key)), self)
def by_id(self, id):
def by_id(self, id: int) -> ifcopenshell.entity_instance:
"""Return an IFC entity instance filtered by IFC ID.
:param id: STEP numerical identifier
:type id: int
:raises RuntimeError: If `id` is not found.
:returns: An ifcopenshell.entity_instance.entity_instance
:rtype: ifcopenshell.entity_instance.entity_instance
"""
return self[id]
def by_guid(self, guid):
def by_guid(self, guid: str) -> ifcopenshell.entity_instance:
"""Return an IFC entity instance filtered by IFC GUID.
:param guid: GlobalId value in 22-character encoded form
:type guid: string
:raises RuntimeError: If `guid` is not found.
:returns: An ifcopenshell.entity_instance.entity_instance
:rtype: ifcopenshell.entity_instance.entity_instance
"""
return self[guid]
def add(self, inst, _id=None):
def add(self, inst: ifcopenshell.entity_instance, _id: int = None) -> ifcopenshell.entity_instance:
"""Adds an entity including any dependent entities to an IFC file.
If the entity already exists, it is not re-added. Existence of entity is checked by it's `.identity()`.
:param inst: The entity instance to add
:type inst: ifcopenshell.entity_instance.entity_instance
:returns: An ifcopenshell.entity_instance.entity_instance
:rtype: ifcopenshell.entity_instance.entity_instance
"""
If the entity already exists, it is not re-added."""
if self.transaction:
max_id = self.wrapped_data.getMaxId()
inst.wrapped_data.this.disown()
@@ -425,7 +443,7 @@ class file(object):
[self.transaction.store_create(e) for e in reversed(added_elements)]
return result
def by_type(self, type, include_subtypes=True):
def by_type(self, type: str, include_subtypes=True) -> List[ifcopenshell.entity_instance]:
"""Return IFC objects filtered by IFC Type and wrapped with the entity_instance class.
If an IFC type class has subclasses, all entities of those subclasses are also returned.
@@ -441,7 +459,9 @@ class file(object):
return [entity_instance(e, self) for e in self.wrapped_data.by_type(type)]
return [entity_instance(e, self) for e in self.wrapped_data.by_type_excl_subtypes(type)]
def traverse(self, inst, max_levels=None, breadth_first=False):
def traverse(
self, inst: ifcopenshell.entity_instance, max_levels=None, breadth_first=False
) -> List[ifcopenshell.entity_instance]:
"""Get a list of all referenced instances for a particular instance including itself
:param inst: The entity instance to get all sub instances
@@ -463,7 +483,9 @@ class file(object):
return [entity_instance(e, self) for e in fn(inst.wrapped_data, max_levels)]
def get_inverse(self, inst, allow_duplicate=False, with_attribute_indices=False):
def get_inverse(
self, inst: ifcopenshell.entity_instance, allow_duplicate=False, with_attribute_indices=False
) -> List[ifcopenshell.entity_instance]:
"""Return a list of entities that reference this entity
:param inst: The entity instance to get inverse relationships
@@ -488,7 +510,7 @@ class file(object):
return set(inverses)
def get_total_inverses(self, inst):
def get_total_inverses(self, inst: ifcopenshell.entity_instance) -> int:
"""Returns the number of entities that reference this entity
:param inst: The entity instance to get inverse relationships
@@ -498,7 +520,7 @@ class file(object):
"""
return self.wrapped_data.get_total_inverses(inst.wrapped_data)
def remove(self, inst):
def remove(self, inst: ifcopenshell.entity_instance) -> None:
"""Deletes an IFC object in the file.
Attribute values in other entity instances that reference the deleted
@@ -573,7 +595,7 @@ class file(object):
return
@staticmethod
def from_string(s):
def from_string(s: str) -> ifcopenshell.entity_instance:
return file(ifcopenshell_wrapper.read(s))
@staticmethod
@@ -44,6 +44,26 @@ class TestAppendAsset(test.bootstrap.IFC4):
ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element)
assert len(self.file.by_type("IfcWallType")) == 1
def test_reuse_an_existing_context_if_it_was_added_from_library_previously(self):
library = ifcopenshell.api.run("project.create_file")
project = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcProject")
lib_context = ifcopenshell.api.run("context.add_context", library, context_type="Model")
self.file.add(project) # will add project and it's contexts
material = ifcopenshell.api.run("material.add_material", library, name="Material")
style = ifcopenshell.api.run("style.add_style", library)
ifcopenshell.api.run(
"style.assign_material_style", library, material=material, style=style, context=lib_context
)
ifcopenshell.api.run("project.append_asset", self.file, library=library, element=material)
assert len(self.file.by_type("IfcMaterial")) == 1
assert len(self.file.by_type("IfcGeometricRepresentationContext")) == 1
context = self.file.by_type("IfcMaterial")[0].HasRepresentation[0].Representations[0].ContextOfItems
# make sure it's still valid
assert context.WorldCoordinateSystem
def test_append_a_single_type_product_even_though_an_inverse_material_relationship_is_shared(self):
library = ifcopenshell.api.run("project.create_file")
element = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType")
@@ -202,24 +222,40 @@ class TestAppendAsset(test.bootstrap.IFC4):
style = ifcopenshell.api.run("style.add_style", library)
context = ifcopenshell.api.run("context.add_context", library, context_type="Model")
ifcopenshell.api.run("style.assign_material_style", library, material=material, style=style, context=context)
ifcopenshell.api.run("project.append_asset", self.file, library=library, element=material)
assert len(self.file.by_type("IfcMaterial")) == 1
assert len(self.file.by_type("IfcGeometricRepresentationContext")) == 1
context = self.file.by_type("IfcMaterial")[0].HasRepresentation[0].Representations[0].ContextOfItems
assert context == file_context
# make sure it's still valid
assert context.WorldCoordinateSystem
def test_append_a_material_with_a_representation_and_reuse_an_existing_subcontext(self):
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
file_context = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
file_subcontext = ifcopenshell.api.run("context.add_context", self.file, parent=file_context, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW")
file_subcontext = ifcopenshell.api.run(
"context.add_context",
self.file,
parent=file_context,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
)
library = ifcopenshell.api.run("project.create_file")
ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcProject")
material = ifcopenshell.api.run("material.add_material", library, name="Material")
style = ifcopenshell.api.run("style.add_style", library)
context = ifcopenshell.api.run("context.add_context", library, context_type="Model")
subcontext = ifcopenshell.api.run("context.add_context", library, parent=context, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW")
subcontext = ifcopenshell.api.run(
"context.add_context",
library,
parent=context,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
)
ifcopenshell.api.run("style.assign_material_style", library, material=material, style=style, context=subcontext)
ifcopenshell.api.run("project.append_asset", self.file, library=library, element=material)
assert len(self.file.by_type("IfcMaterial")) == 1
@@ -227,6 +263,7 @@ class TestAppendAsset(test.bootstrap.IFC4):
assert len(self.file.by_type("IfcGeometricRepresentationSubContext", include_subtypes=False)) == 1
subcontext = self.file.by_type("IfcMaterial")[0].HasRepresentation[0].Representations[0].ContextOfItems
assert subcontext == file_subcontext
# make sure it's still valid
assert subcontext.ParentContext.WorldCoordinateSystem
def test_append_a_material_with_a_representation_and_reuse_an_existing_context_by_a_new_subcontext(self):
@@ -238,8 +275,14 @@ class TestAppendAsset(test.bootstrap.IFC4):
material = ifcopenshell.api.run("material.add_material", library, name="Material")
style = ifcopenshell.api.run("style.add_style", library)
context = ifcopenshell.api.run("context.add_context", library, context_type="Model")
subcontext = ifcopenshell.api.run("context.add_context", library, context_type="Model",
context_identifier="Body", target_view="MODEL_VIEW", parent=context)
subcontext = ifcopenshell.api.run(
"context.add_context",
library,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
parent=context,
)
ifcopenshell.api.run("style.assign_material_style", library, material=material, style=style, context=subcontext)
ifcopenshell.api.run("project.append_asset", self.file, library=library, element=material)
assert len(self.file.by_type("IfcMaterial")) == 1
+3 -2
View File
@@ -25,9 +25,10 @@ import typing
import inspect
import collections
import importlib
from typing import Union
def execute(args):
def execute(args: dict) -> Union[ifcopenshell.file, str]:
"""Execute a patch recipe
The details of how the patch recipe is executed depends on the definition of
@@ -80,7 +81,7 @@ def execute(args):
return output
def write(output, filepath):
def write(output: Union[ifcopenshell.file, str], filepath: str) -> None:
"""Write the output of an IFC patch to a file
Typically a patch output would be a patched IFC model file object, or as a
+2 -1
View File
@@ -18,7 +18,6 @@
import ifcopenshell
import ifcopenshell.util.element
from toposort import toposort_flatten as toposort
class Patcher:
@@ -53,6 +52,8 @@ class Patcher:
self.optimized_file = ifcopenshell.file(schema=self.file.schema)
def patch(self):
from toposort import toposort_flatten as toposort
def generate_instances_and_references():
"""
Generator which yields an entity id and
+3
View File
@@ -493,6 +493,9 @@ namespace {
for (auto& prop : *props) {
if (prop->declaration().is("IfcPropertySingleValue")) {
std::string name = *((IfcUtil::IfcBaseEntity*) prop)->get("Name");
if (((IfcUtil::IfcBaseEntity*) prop)->get("NominalValue")->isNull()) {
continue;
}
IfcUtil::IfcBaseClass* v = *((IfcUtil::IfcBaseEntity*) prop)->get("NominalValue");
auto value = v->data().getArgument(0);
if (value->type() == IfcUtil::Argument_STRING) {