Compare commits

...

58 Commits

Author SHA1 Message Date
Andrej730 fc0ffd1c02 fix a typo #4596 😬 2024-05-01 15:40:45 +05:00
Bruno Perdigão 97e0c43702 Merge pull request #4440 from brunoperdigao/fix-normal-duplication-of-linked-aggregate
Fix normal duplication of linked aggregate
2024-04-30 17:01:44 -03:00
Andrej730 8dde98f543 fix #4592 (after 0da3270)
Before IFC4 Shape Aspects counldn't be part of IfcRepresentationMap
2024-04-29 15:22:26 +05:00
Ryan Schultz 9962a0c905 small fix to previous commit. Solves https://github.com/IfcOpenShell/IfcOpenShell/commit/4b56b341243d6b0eed82e8533848b124e82e1344#commitcomment-141442796 2024-04-28 18:57:42 -05:00
Ryan Schultz 4b56b34124 Tweaked UI for Linked Aggregates https://imgur.com/a/rExISKO 2024-04-28 10:34:52 -05:00
Thomas Krijnen f857c513c5 submodule 2024-04-28 10:17:57 +02:00
Kaare Hansen 41df6410bd Comment std::cout causing console spam 2024-04-28 10:11:32 +02:00
Ryan Schultz dcbd6a925d small fix for #4361. remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615 2024-04-27 18:13:38 -05:00
Ryan Schultz 3848a21136 fix #4361: Have the aggregate empty turn on too, if at least one object in the aggregate, is turned on as well 2024-04-27 11:31:39 -05:00
Andrej730 8af37cb4ec Fix #4586 occurred after e14d96e6e 2024-04-27 12:03:56 +05:00
Ryan Schultz 44c28e9a4c fix #4327: reorder format as well 2024-04-26 20:48:59 -05:00
Ryan Schultz 677324927c fix #4431: prompt for name, when creating aggregate 2024-04-26 20:09:20 -05:00
Massimo Fabbro d212344c54 Covering tool more user friendly UI ... 2024-04-26 17:19:28 +02:00
Massimo Fabbro 74da327803 Fix create space from cursor bug where spaces were created always in zero elevation 2024-04-26 17:19:28 +02:00
Massimo Fabbro f021041b31 Remove the (maybe) unnecessary poll functions for spatial tool and cleaner error messages 2024-04-26 17:19:28 +02:00
Massimo Fabbro c05d2df116 now spaces created with spatial tool have relative zero elevation origin point 2024-04-26 17:19:28 +02:00
Andrej730 2b926f7906 more descriptive error setting non-optional attributes with None
Example:

```
import ifcopenshell
import ifcopenshell.api.owner.settings

ifc_file = ifcopenshell.file(schema="IFC2X3")
ifc_file.createIfcOwnerHistory(OwningUser=None)

# Before:
# Traceback (most recent call last):
#   File "\test.py", line 16, in <module>
#     ifcopenshell.api.run("owner.create_owner_history", ifc_file)
#   File "\ifcopenshell\api\__init__.py", line 172, in run
#     result = usecase_class(ifc_file, **settings).execute()
#              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#   File "\ifcopenshell\api\owner\create_owner_history.py", line 111, in execute
#     return self.file.create_entity(
#            ^^^^^^^^^^^^^^^^^^^^^^^^
#   File "\ifcopenshell\file.py", line 350, in create_entity
#     e[idx] = arg
#     ~^^^^^
#   File "\ifcopenshell\entity_instance.py", line 305, in __setitem__
#     self.wrapped_data.setArgumentAsNull(idx)
#   File "\ifcopenshell\ifcopenshell_wrapper.py", line 5285, in setArgumentAsNull
#     return _ifcopenshell_wrapper.entity_instance_setArgumentAsNull(self, i)
#            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# RuntimeError: Attribute not set

# After:
# Traceback (most recent call last):
#   File "\test.py", line 16, in <module>
#     ifcopenshell.api.run("owner.create_owner_history", ifc_file)
#   File "\ifcopenshell\api\__init__.py", line 172, in run
#     result = usecase_class(ifc_file, **settings).execute()
#              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#   File "\ifcopenshell\api\owner\create_owner_history.py", line 111, in execute
#     return self.file.create_entity(
#            ^^^^^^^^^^^^^^^^^^^^^^^^
#   File "\ifcopenshell\file.py", line 350, in create_entity
#     e[idx] = arg
#     ~^^^^^
#   File "\ifcopenshell\entity_instance.py", line 316, in __setitem__
#     raise ValueError(
# ValueError: attribute 'OwningUser' is not optional for entity instance of type 'IFC2X3.IfcOwnerHistory'
```
2024-04-26 18:23:19 +05:00
Andrej730 3ac69043d6 bbim to provide default user and organisation in ifc2x3 #4574
previously every operation that would try to create owner history would fail with the error below if there wasn't IfcPersonAndOrganisation in the project.
2024-04-26 18:23:19 +05:00
Andrej730 f726708815 edit_object_placement - small optimizations 2024-04-26 18:23:19 +05:00
Andrej730 21e153087a edit_object_placement - adherence support
Update edit_object_placement.py
2024-04-26 18:23:18 +05:00
Andrej730 31d85b715a typing 2024-04-26 18:23:18 +05:00
Andrej730 344cc1fb7d file.by_type docs note 2024-04-26 11:49:44 +05:00
Andrej730 1cea3d21e8 BBIM support importing IfcRelAdheresToElement #4565 2024-04-26 11:44:32 +05:00
Andrej730 640320f70d shape_builder - create non-optional position for profiles in ifc2x3 2024-04-25 18:05:47 +05:00
Andrej730 29352092aa fix issue creating 3d location for 2d placement in ifc2x3 2024-04-25 18:05:47 +05:00
Andrej730 5b877b549a fix numpy typing #4579
On older numpy versions, np.ndarray was less forgiving and wasn't allowing passing 1 argument instead of required 2.

And turned out numpy doesn't yet have typing for shapes (https://github.com/numpy/numpy/issues/16544), so all matrices and other shapes specified as `npt.NDArray[np.float64]`.

Fixed type discrepancies for `get_edges` and `get_faces` and also had to fix `import_ifc` as Blender apparently has problems with storing np.int32 in custom attributes (https://projects.blender.org/blender/blender/issues/121072), tested that Blender is okay with np.int32 in other cases we had (addressing BMesh.verts[i] where `i` is np.int32).
2024-04-25 18:05:47 +05:00
Andrej730 721466c429 typing 2024-04-25 18:05:47 +05:00
Andrej730 cce2fee1fa More descriptive error setting non-present attributes
Example:

```
import ifcopenshell

ifc_file = ifcopenshell.file(schema="IFC2X3")
# ifc_file.begin_transaction()

wall = ifc_file.createIfcWall()
wall.Identification = "25"

# Before:
# Traceback (most recent call last):
#   File "test.py", line 4, in <module>
#     wall.Identification = "25"
#     ^^^^^^^^^^^^^^^^^^^
#   File "\ifcopenshell\entity_instance.py", line 279, in __setattr__
#     self[index] = value
#     ~~~~^^^^^^^
#   File "\ifcopenshell\entity_instance.py", line 293, in __setitem__
#     method = self.method_list[idx]
#              ~~~~~~~~~~~~~~~~^^^^^
# IndexError: list index out of range

# or this (if file had a transaction going)
#   File "\ifcopenshell\entity_instance.py", line 279, in __setattr__
#     self[index] = value
#     ~~~~^^^^^^^
#   File "\ifcopenshell\entity_instance.py", line 288, in __setitem__
#     self.wrapped_data.file.transaction.store_edit(self, idx, value)
#   File "\ifcopenshell\file.py", line 101, in store_edit
#     "old": self.serialise_value(element, element[index]),
#                                          ~~~~~~~^^^^^^^
#   File "\ifcopenshell\entity_instance.py", line 283, in __getitem__
#     raise IndexError("Attribute index {} out of range for instance of type {}".format(key, self.is_a()))
# IndexError: Attribute index 4294967295 out of range for instance of type IfcWall

# After:
# Traceback (most recent call last):
#   File "test.py", line 4, in <module>
#     wall.Identification = "25"
#     ^^^^^^^^^^^^^^^^^^^
#   File "ifcopenshell\entity_instance.py", line 283, in __setattr__
#     raise AttributeError(
# AttributeError: entity instance of type 'IFC2X3.IfcWall' has no attribute 'Identification'

```
2024-04-25 18:05:47 +05:00
Andrej730 087d55f02a fix editing sheets in ifc2x3 #4576 2024-04-25 18:05:47 +05:00
Andrej730 f1037de14d small fix for 60a70f523 2024-04-25 18:05:46 +05:00
Ryan Schultz 4711672170 small tweak to 3ff31b577c and cd013079f4 - duplicate material and duplicate style have '_copy' suffix. 2024-04-24 21:48:26 -05:00
Bruno Postle c0b4e099af fix sheet regeneration missing drawing Identifier
Was passing LAYOUT document reference to sheeter instead of
DRAWING document reference
2024-04-24 23:05:44 +01:00
Andrej730 f41fbf24c3 small fix for e14d96e6e 2024-04-24 18:19:20 +05:00
Andrej730 e14d96e6ee Use .Name instead of .Description if IFC2X3 for sheets #4576
As Description was added only in IFC4
2024-04-24 18:17:37 +05:00
Andrej730 716a33f347 convert_file_length_units - fix issue converting to imperial units
ifcpatch ConvertLengthUnit - change used units from plural to singular names, to make it consistent across the api

more test coverage - test converting to more units and back from them
2024-04-24 15:15:30 +05:00
Andrej730 c5f084a4af ConvertLengthUnit test 2024-04-24 14:12:13 +05:00
Andrej730 606a0b46f2 MergeProject - support merging projects with different units 2024-04-24 14:12:13 +05:00
Andrej730 1c729a74d1 util.unit to support converting aggregate of aggregates 2024-04-24 14:12:13 +05:00
Andrej730 db31574103 typing 2024-04-24 14:12:13 +05:00
stefkeB d03b47e1e6 Fix HDF5 conditional inclusion in GeomTree
IfcGeomTree uses HDF5, but this is an optional module, so we have to wrap it with #ifdef WITH_HDF5
2024-04-23 18:46:10 +02:00
Ryan Schultz ae62ac2777 small tweak to b8a9674483 - duplicate profile has '_copy' suffix. 2024-04-23 07:42:02 -05:00
Andrej730 998f9c6a1e make styles ui more similar to profiles and materials ui
before - https://i.imgur.com/bTbyt2t.png
after - https://i.imgur.com/U53h8Yh.png
2024-04-23 16:39:49 +05:00
Andrej730 3ff31b577c bim.duplicate_style 2024-04-23 16:39:49 +05:00
Andrej730 cd013079f4 bim.duplicate_material 2024-04-23 16:39:49 +05:00
Andrej730 b8a9674483 bim.duplicate_profile
simple operator for profile duplication
example - https://imgur.com/a/dsP78iV
2024-04-23 16:39:49 +05:00
Andrej730 8beb1bef98 preserve selected profile index removing profiles 2024-04-23 16:39:38 +05:00
Andrej730 377676f881 dev environment - add libs/desktop and bsdd 2024-04-23 15:23:16 +05:00
Andrej730 4efe284338 use IFC4X3_ADD2 for ifcopenshell.schema_by_name
as it is the main IFC4X3 schema
2024-04-22 17:30:25 +05:00
Andrej730 34a4e3f37a util.doc - use release schema version for IFC4X3
As it will be more like to be available in the ifcopenshell package. Related to #4565
2024-04-22 17:30:25 +05:00
Andrej730 889c9a9e9f typing 2024-04-22 17:30:25 +05:00
Andrej730 9e79499532 small optimization 2024-04-22 17:30:24 +05:00
Kristof Semjen 589b98053e Fixes #4261
SWIG_Python_str_AsChar and SWIG_Python_str_DelForPy3 are no longer available in swig 4.2 (see : https://github.com/swig/swig/commit/f89dd59d4b82ece899087682fdb86e94d2611513 ),
this commit fixes the build for swig versions > 4.2.
2024-04-21 16:22:56 +02:00
Bruno Postle fc5bb230a9 Abandon SVG layout generation on missing drawings
Drawings need to be generated before layout can be generated, so
remove a partial failed layout caused by missing drawings.
2024-04-21 11:12:24 +01:00
Bruno Perdigão 01361cc8f2 Merge branch 'v0.7.0' into fix-normal-duplication-of-linked-aggregate 2024-04-18 21:00:39 -03:00
Bruno Perdigão e6bef16c45 small deletions 2024-03-18 23:39:06 -03:00
Bruno Perdigão 4829c4c02b fix: when subaggregate that is a linked aggregate is unassinged from the main aggregate, it remains a linked aggregate. 2024-03-16 15:27:04 -03:00
Bruno Perdigão 3bf4a8ef52 small refactor 2024-03-16 12:44:26 -03:00
Bruno Perdigão c95a47ca97 fix #4430. Shift+D whem selecting all the objects of a linked aggregate creates a normal copy that is not linked 2024-03-15 21:34:27 -03:00
72 changed files with 1302 additions and 530 deletions
+16 -3
View File
@@ -223,7 +223,7 @@ def redo_post(scene):
tool.Ifc.rebuild_element_maps()
def get_application(ifc):
def get_application(ifc: ifcopenshell.file) -> ifcopenshell.entity_instance:
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
version = get_application_version()
for element in ifc.by_type("IfcApplication"):
@@ -238,7 +238,20 @@ def get_application(ifc):
)
def get_application_version():
def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]:
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
if pao := next(iter(ifc.by_type("IfcPersonAndOrganization")), None):
return pao
elif ifc.schema == "IFC2X3":
if (person := next(iter(ifc.by_type("IfcPerson")), None)) is None:
person = tool.Ifc.run("owner.add_person")
if (organization := next(iter(ifc.by_type("IfcOrganization")), None)) is None:
organization = tool.Ifc.run("owner.add_organisation")
pao = tool.Ifc.run("owner.add_person_and_organisation", person=person, organisation=organization)
return pao
def get_application_version() -> str:
return ".".join(
[
str(x)
@@ -279,7 +292,7 @@ def load_post(scene):
key=key, owner=global_subscription_owner, args=(area,), notify=viewport_shading_changed_callback
)
ifcopenshell.api.owner.settings.get_user = lambda ifc: core_owner.get_user(tool.Owner)
ifcopenshell.api.owner.settings.get_user = get_user
ifcopenshell.api.owner.settings.get_application = get_application
AuthoringData.type_thumbnails = {}
+39 -26
View File
@@ -34,6 +34,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.geolocation
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape
import blenderbim.tool as tool
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
from itertools import chain, accumulate
@@ -199,7 +200,7 @@ class MaterialCreator:
class IfcImporter:
def __init__(self, ifc_import_settings):
def __init__(self, ifc_import_settings: IfcImportSettings):
self.ifc_import_settings = ifc_import_settings
self.diff = None
self.file: ifcopenshell.file = None
@@ -430,7 +431,7 @@ class IfcImporter:
self.annotations = set([a for a in self.file.by_type("IfcAnnotation")])
self.annotations -= drawing_annotations
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement")]
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
if self.ifc_import_settings.is_coordinating:
self.elements = [e for e in self.elements if e.Representation]
@@ -725,7 +726,7 @@ class IfcImporter:
if len(subelement.Coordinates) == 3 and self.is_point_far_away(subelement, is_meters=False):
return True
def apply_blender_offset_to_matrix_world(self, obj, matrix):
def apply_blender_offset_to_matrix_world(self, obj: bpy.types.Object, matrix: np.ndarray) -> mathutils.Matrix:
props = bpy.context.scene.BIMGeoreferenceProperties
if props.has_blender_offset:
if obj.data and obj.data.get("has_cartesian_point_offset", None):
@@ -862,7 +863,6 @@ class IfcImporter:
else:
self.create_generic_elements(self.spatial_elements, unselectable=False)
def create_elements(self) -> None:
self.create_generic_elements(self.elements)
tmp = self.context_settings
@@ -953,7 +953,9 @@ class IfcImporter:
self.create_product(element, mesh=mesh)
def create_products(
self, products, settings: Optional[ifcopenshell.geom.main.settings] = None
self,
products: set[ifcopenshell.entity_instance],
settings: Optional[ifcopenshell.geom.main.settings] = None,
) -> set[ifcopenshell.entity_instance]:
results = set()
if not products:
@@ -1627,30 +1629,34 @@ class IfcImporter:
if self.ifc_import_settings.has_filter:
rel_aggregates = set()
for element in self.elements:
if element.IsDecomposedBy:
rel_aggregates.add(element.IsDecomposedBy[0])
elif element.Decomposes:
rel_aggregates.add(element.Decomposes[0])
elif getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy
if [e for e in element.IsNestedBy[0].RelatedObjects if not e.is_a("IfcPort")]:
rel_aggregates.add(element.IsNestedBy[0])
elif getattr(element, "Nests", []):
rel_aggregates.add(element.Nests[0])
if decomposed_by := element.IsDecomposedBy:
rel_aggregates.add(decomposed_by[0])
elif decomposes := element.Decomposes:
rel_aggregates.add(decomposes[0])
elif nested_by := getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy
if next((e for e in nested_by[0].RelatedObjects if not e.is_a("IfcPort")), None):
rel_aggregates.add(nested_by[0])
elif nests := getattr(element, "Nests", []):
rel_aggregates.add(nests[0])
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
rel_aggregates.add(element.AdheresToElement[0])
else:
rel_aggregates = [
r
for r in self.file.by_type("IfcRelAggregates")
if r.RelatingObject.is_a("IfcElement") or r.RelatingObject.is_a("IfcElementType")
if (relating_obj := r.RelatingObject).is_a("IfcElement") or relating_obj.is_a("IfcElementType")
] + [
r
for r in self.file.by_type("IfcRelNests")
if (
r.RelatingObject.is_a("IfcElement")
or r.RelatingObject.is_a("IfcElementType")
or (r.RelatingObject.is_a("IfcPositioningElement") and not r.RelatingObject.is_a("IfcGrid"))
(relating_obj := r.RelatingObject).is_a("IfcElement")
or relating_obj.is_a("IfcElementType")
or (relating_obj.is_a("IfcPositioningElement") and not relating_obj.is_a("IfcGrid"))
)
and [e for e in r.RelatedObjects if not e.is_a("IfcPort")]
]
if self.file.schema == "IFC4X3":
rel_aggregates += [r for r in self.file.by_type("IfcRelAdheresToElement")]
if len(rel_aggregates) > 10000:
# More than 10,000 collections makes Blender unhappy
@@ -1660,7 +1666,9 @@ class IfcImporter:
aggregates: dict[str, dict] = {}
for rel_aggregate in rel_aggregates:
element: ifcopenshell.entity_instance = rel_aggregate.RelatingObject
element: ifcopenshell.entity_instance = getattr(rel_aggregate, "RelatingObject", None) or getattr(
rel_aggregate, "RelatingElement"
)
collection = bpy.data.collections.new(tool.Loader.get_name(element))
aggregates[element.GlobalId] = {"element": element, "collection": collection}
self.collections[element.GlobalId] = collection
@@ -1761,6 +1769,9 @@ class IfcImporter:
elif getattr(element, "Nests", None) and not element.is_a("IfcPort"):
nest = ifcopenshell.util.element.get_nest(element)
return self.collections[nest.GlobalId].objects.link(obj)
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
adherend = element.AdheresToElement[0].RelatingElement
return self.collections[adherend.GlobalId].objects.link(obj)
return self.place_object_in_spatial_decomposition_collection(element, obj)
@@ -1810,7 +1821,7 @@ class IfcImporter:
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
return rel.RelatingGroup
def get_element_matrix(self, element: ifcopenshell.entity_instance) -> np.array:
def get_element_matrix(self, element: ifcopenshell.entity_instance) -> np.ndarray:
if isinstance(element, ifcopenshell.sqlite_entity):
result = self.geometry_cache["shapes"][element.id()]["matrix"]
else:
@@ -1932,7 +1943,9 @@ class IfcImporter:
# See bug 3546
# ios_edges holds true edges that aren't triangulated.
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry)))
#
# we do `.tolist()` because Blender can't assign `np.int32` to it's custom attributes
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry).tolist()))
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
@@ -1959,14 +1972,14 @@ class IfcImporter:
print(traceback.format_exc())
def a2p(self, o, z, x):
def a2p(self, o: mathutils.Vector, z: mathutils.Vector, x: mathutils.Vector) -> mathutils.Matrix:
y = z.cross(x)
r = mathutils.Matrix((x, y, z, o))
r.resize_4x4()
r.transpose()
return r
def get_axis2placement(self, plc):
def get_axis2placement(self, plc: ifcopenshell.entity_instance) -> mathutils.Matrix:
if plc.is_a("IfcAxis2Placement3D"):
z = mathutils.Vector(plc.Axis.DirectionRatios if plc.Axis else (0, 0, 1))
x = mathutils.Vector(plc.RefDirection.DirectionRatios if plc.RefDirection else (1, 0, 0))
@@ -1986,7 +1999,7 @@ class IfcImporter:
o = plc.LocalOrigin.Coordinates
return self.a2p(o, z, x)
def get_local_placement(self, plc):
def get_local_placement(self, plc: Optional[ifcopenshell.entity_instance] = None) -> mathutils.Matrix:
if plc is None:
return mathutils.Matrix()
if plc.PlacementRelTo is None:
@@ -2001,11 +2014,11 @@ class IfcImporter:
bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id())
break
def link_element(self, element, obj):
def link_element(self, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
self.added_data[element.id()] = obj
tool.Ifc.link(element, obj)
def set_matrix_world(self, obj, matrix_world):
def set_matrix_world(self, obj: bpy.types.Object, matrix_world: mathutils.Matrix) -> None:
obj.matrix_world = matrix_world
tool.Geometry.record_object_position(obj)
@@ -96,12 +96,13 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
)
# Removes Pset related to Linked Aggregates
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Linked_Aggregate")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
if not element.is_a('IfcElementAssembly'):
pset = ifcopenshell.util.element.get_pset(element, 'BBIM_Linked_Aggregate')
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator):
"""Enable editing aggregation relationship"""
@@ -132,6 +133,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
ifc_class: bpy.props.StringProperty(name="IFC Class", default="IfcElementAssembly")
aggregate_name: bpy.props.StringProperty(name="Name", default="Default_Name")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -139,13 +141,15 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
def draw(self, context):
row = self.layout
row.prop(self, "ifc_class")
row = self.layout
row.prop(self, "aggregate_name")
def _execute(self, context):
try:
ifc_class = tool.Ifc.schema().declaration_by_name(self.ifc_class).name()
except:
return
aggregate = self.create_aggregate(context, ifc_class)
aggregate = self.create_aggregate(context, ifc_class, self.aggregate_name)
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -173,8 +177,8 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
)
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
def create_aggregate(self, context, ifc_class):
aggregate = bpy.data.objects.new("Assembly", None)
def create_aggregate(self, context, ifc_class, aggregate_name):
aggregate = bpy.data.objects.new(aggregate_name, None)
aggregate.location = context.scene.cursor.location
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class=ifc_class)
return aggregate
@@ -20,6 +20,7 @@ from bpy.types import Panel
from blenderbim.bim.module.aggregate.data import AggregateData
from blenderbim.bim.module.group.data import GroupsData, ObjectGroupsData
from blenderbim.bim.ifc import IfcStore
import blenderbim.tool as tool
class BIM_PT_aggregate(Panel):
@@ -131,22 +132,29 @@ class BIM_PT_linked_aggregate(Panel):
if not AggregateData.is_loaded:
AggregateData.load()
props = context.active_object.BIMObjectAggregateProperties
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMObjectAggregateProperties
row = layout.row(align=True)
row.label(text="Advanced Users Only", icon="ERROR")
row = layout.row(align=True)
if type(AggregateData.data['total_linked_aggregate']) is int:
if AggregateData.data['total_linked_aggregate'] > 0:
row.label(text=f"{AggregateData.data['total_linked_aggregate']} Linked Aggregates")
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_DATA_POINTCLOUD")
op.select_parts = False
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_OB_POINTCLOUD")
op.select_parts = True
row.operator("bim.refresh_linked_aggregate", text="", icon="FILE_REFRESH")
op = row.operator("bim.break_link_to_other_aggregates", text="", icon="X")
if element.Decomposes:
Number_Linked_Aggregates = AggregateData.data['total_linked_aggregate']
if not Number_Linked_Aggregates:
row.label(text="Not a Linked Aggregate")
else:
row.label(text=f"{Number_Linked_Aggregates} Linked Aggregates")
op = row.operator("bim.object_duplicate_move_linked_aggregate", text="", icon="DUPLICATE")
if type(Number_Linked_Aggregates) is int:
if Number_Linked_Aggregates > 0:
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_DATA_POINTCLOUD")
op.select_parts = False
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_OB_POINTCLOUD")
op.select_parts = True
row.operator("bim.refresh_linked_aggregate", text="", icon="FILE_REFRESH")
op = row.operator("bim.break_link_to_other_aggregates", text="", icon="X")
else:
row.label(text="No Linked Aggregates")
row.label(text="Not an Aggregate")
@@ -20,6 +20,7 @@ import bpy
import bmesh
import logging
import shapely
import shapely.ops
import mathutils
import numpy as np
import multiprocessing
@@ -107,59 +107,88 @@ class CoveringToolUI:
# elif element and bpy.context.selected_objects and element.is_a("IfcSpace"):
# op. = row.operator("bim.add_istance_flooring_from_spaces"):
if (type_material_usage == "IfcMaterialLayerSet" and
not bpy.context.selected_objects):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
if tool.Ifc.get_entity(collection_obj):
if AuthoringData.data["predefined_type"] == "FLOORING":
op = row.operator("bim.add_instance_flooring_covering_from_cursor")
elif AuthoringData.data["predefined_type"] == "CEILING":
op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
else:
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
else:
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
elif (AuthoringData.data["predefined_type"] == "FLOORING" and
type_material_usage == "IfcMaterialLayerSet" and
element and
bpy.context.selected_objects and
element.is_a("IfcWall")):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
elif (AuthoringData.data["predefined_type"] == "CEILING" and
type_material_usage == "IfcMaterialLayerSet" and
element and
bpy.context.selected_objects and
element.is_a("IfcWall")):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
elif (element and
bpy.context.selected_objects and
element.is_a("IfcCovering") and
# AuthoringData.data["predefined_type"] == "FLOORING" and
AuthoringData.data["active_material_usage"] == "LAYER3"):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
op = row.operator("bim.regen_selected_covering_object")
# if (type_material_usage == "IfcMaterialLayerSet" and
# not bpy.context.selected_objects):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# if tool.Ifc.get_entity(collection_obj):
# if AuthoringData.data["predefined_type"] == "FLOORING":
# op = row.operator("bim.add_instance_flooring_covering_from_cursor")
# elif AuthoringData.data["predefined_type"] == "CEILING":
# op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
# else:
# op = row.operator("bim.add_constr_type_instance", text="Add")
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
#
# else:
# op = row.operator("bim.add_constr_type_instance", text="Add")
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
#
# elif (AuthoringData.data["predefined_type"] == "FLOORING" and
# type_material_usage == "IfcMaterialLayerSet" and
# element and
# bpy.context.selected_objects and
# element.is_a("IfcWall")):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# op = row.operator("bim.add_instance_flooring_coverings_from_walls")
#
# elif (AuthoringData.data["predefined_type"] == "CEILING" and
# type_material_usage == "IfcMaterialLayerSet" and
# element and
# bpy.context.selected_objects and
# element.is_a("IfcWall")):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
#
# elif (element and
# bpy.context.selected_objects and
# element.is_a("IfcCovering") and
## AuthoringData.data["predefined_type"] == "FLOORING" and
# AuthoringData.data["active_material_usage"] == "LAYER3"):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_G")
# op = row.operator("bim.regen_selected_covering_object")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_flooring_covering_from_cursor")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
op = row.operator("bim.regen_selected_covering_object")
# elif AuthoringData.data["predefined_type"] == "CEILING":
# row = cls.layout.row(align=True)
@@ -174,14 +203,14 @@ class CoveringToolUI:
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
else:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
# else:
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# op = row.operator("bim.add_constr_type_instance", text="Add")
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
@classmethod
def draw_type_selection_interface(cls):
@@ -72,7 +72,7 @@ class ReorderCsvAttribute(bpy.types.Operator):
def execute(self, context):
old = context.scene.CsvProperties.csv_attributes[self.old_index]
new = context.scene.CsvProperties.csv_attributes[self.new_index]
props = ["name", "header", "sort", "group", "varies_value", "summary"]
props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"]
for prop in props:
value = getattr(new, prop)
setattr(new, prop, getattr(old, prop))
@@ -1241,12 +1241,14 @@ class AddDrawingToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
"Location": drawing_reference.Location,
"Description": "DRAWING",
}
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1
),
Location=drawing_reference.Location,
Description="DRAWING",
)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
@@ -1314,9 +1316,10 @@ class CreateSheets(bpy.types.Operator, Operator):
has_sheet_reference = False
for reference in tool.Drawing.get_document_references(sheet):
if reference.Description == "SHEET":
reference_description = tool.Drawing.get_reference_description(reference)
if reference_description == "SHEET":
has_sheet_reference = True
elif reference.Description == "RASTER":
elif reference_description == "RASTER":
if reference.Location in raster_references:
raster_references.remove(reference.Location)
else:
@@ -1327,7 +1330,9 @@ class CreateSheets(bpy.types.Operator, Operator):
tool.Ifc.run(
"document.edit_reference",
reference=reference,
attributes={"Location": tool.Ifc.get_relative_uri(svg), "Description": "SHEET"},
attributes=tool.Drawing.generate_reference_attributes(
reference, Location=tool.Ifc.get_relative_uri(svg), Description="SHEET"
),
)
for raster_reference in raster_references:
@@ -1335,7 +1340,9 @@ class CreateSheets(bpy.types.Operator, Operator):
tool.Ifc.run(
"document.edit_reference",
reference=reference,
attributes={"Location": tool.Ifc.get_relative_uri(raster_reference), "Description": "RASTER"},
attributes=tool.Drawing.generate_reference_attributes(
reference, Location=tool.Ifc.get_relative_uri(raster_reference), Description="RASTER"
),
)
svg2pdf_command = context.preferences.addons["blenderbim"].preferences.svg2pdf_command
@@ -1988,12 +1995,14 @@ class AddScheduleToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
"Location": schedule_location,
"Description": "SCHEDULE",
}
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1
),
Location=schedule_location,
Description="SCHEDULE",
)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
@@ -2042,12 +2051,15 @@ class AddReferenceToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "REFERENCE")]) + 1),
"Location": extref_location,
"Description": "REFERENCE",
}
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "REFERENCE")])
+ 1
),
Location=extref_location,
Description="REFERENCE",
)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
@@ -2392,8 +2404,8 @@ class EditSheet(bpy.types.Operator, Operator):
if sheet.is_a("IfcDocumentInformation"):
self.document_type = "SHEET"
self.name = sheet.Name
self.identification = sheet.Identification
elif sheet.is_a("IfcDocumentReference") and sheet.Description == "TITLEBLOCK":
self.identification = sheet.DocumentId if tool.Ifc.get_schema() == "IFC2X3" else sheet.Identification
elif sheet.is_a("IfcDocumentReference") and tool.Drawing.get_reference_description(sheet) == "TITLEBLOCK":
self.document_type = "TITLEBLOCK"
else:
self.document_type = "EMBEDDED"
@@ -2419,7 +2431,7 @@ class EditSheet(bpy.types.Operator, Operator):
if self.document_type == "SHEET":
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
elif self.document_type == "EMBEDDED":
core.rename_reference(tool.Ifc, reference=sheet, identification=self.identification)
core.rename_reference(tool.Ifc, tool.Drawing, reference=sheet, identification=self.identification)
elif self.document_type == "TITLEBLOCK":
titleblock = self.props.titleblock
reference = sheet
@@ -41,7 +41,7 @@ class SheetBuilder:
self.data_dir = None
self.scale = "NTS"
def create(self, layout_path, titleblock_name):
def create(self, layout_path: str, titleblock_name: str) -> None:
root = ET.Element("svg")
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
@@ -76,7 +76,12 @@ class SheetBuilder:
with open(layout_path, "w") as f:
f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" "))
def add_drawing(self, reference, drawing, sheet):
def add_drawing(
self,
reference: ifcopenshell.entity_instance,
drawing: ifcopenshell.entity_instance,
sheet: ifcopenshell.entity_instance,
) -> None:
filename = drawing.Name
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
layout_dir = os.path.dirname(layout_path)
@@ -131,7 +136,7 @@ class SheetBuilder:
)
layout_tree.write(layout_path)
def update_sheet_drawing_sizes(self, sheet):
def update_sheet_drawing_sizes(self, sheet: ifcopenshell.entity_instance) -> None:
ET.register_namespace("", "http://www.w3.org/2000/svg")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -171,7 +176,7 @@ class SheetBuilder:
layout_tree.write(layout_path)
def remove_drawing(self, reference, sheet):
def remove_drawing(self, reference: ifcopenshell.entity_instance, sheet: ifcopenshell.entity_instance) -> None:
ET.register_namespace("", "http://www.w3.org/2000/svg")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -187,7 +192,12 @@ class SheetBuilder:
layout_tree.write(layout_path)
def add_document(self, reference, document, sheet):
def add_document(
self,
reference: ifcopenshell.entity_instance,
document: ifcopenshell.entity_instance,
sheet: ifcopenshell.entity_instance,
) -> None:
view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(document), "svg")
if not os.path.exists(view_path):
tool.Drawing.create_svg_document(document)
@@ -224,7 +234,7 @@ class SheetBuilder:
)
layout_tree.write(layout_path)
def add_view_title(self, x, y, parent, layout_dir):
def add_view_title(self, x: float, y: float, parent: ET.Element, layout_dir: str) -> None:
title_path = os.path.join(layout_dir, "assets", "view-title.svg")
os.makedirs(os.path.dirname(title_path), exist_ok=True)
if not os.path.exists(title_path):
@@ -241,7 +251,7 @@ class SheetBuilder:
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width")))
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
def build(self, sheet):
def build(self, sheet: ifcopenshell.entity_instance) -> dict:
self.references = {"SHEET": None, "RASTER": []}
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -272,7 +282,7 @@ class SheetBuilder:
return self.references
def build_titleblock(self, root, sheet):
def build_titleblock(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
g = self.parse_embedded_svg(image, sheet.get_info())
@@ -285,7 +295,7 @@ class SheetBuilder:
titleblock.append(g)
titleblock.remove(image)
def ensure_drawing_unique_styles(self, svg, drawing_id):
def ensure_drawing_unique_styles(self, svg: ET.Element, drawing_id: int) -> ET.Element:
"""ensures all drawing's classes and ids will be unique for the whole sheet
by adding `drawing_id` based prefix
"""
@@ -313,7 +323,7 @@ class SheetBuilder:
brackets_level -= 1
text += l
def replace_urls(text):
def replace_urls(text: str) -> str:
"""replace urls `url(#marker)` with `url(#prefix-marker)`
since `url(#marker.prefix)` doesn't seem to work
"""
@@ -343,7 +353,7 @@ class SheetBuilder:
return svg
def build_drawings(self, root, sheet):
def build_drawings(self, root: ET.Element, sheet: ifcopenshell.entity_instance):
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'):
drawing_id = int(view.attrib["data-id"])
try:
@@ -390,7 +400,7 @@ class SheetBuilder:
for image in images:
view.remove(image)
def build_documents(self, root, sheet):
def build_documents(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
schedules = root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]')
references = root.findall('{http://www.w3.org/2000/svg}g[@data-type="reference"]')
documents = schedules + references
@@ -427,10 +437,10 @@ class SheetBuilder:
for image in images:
view.remove(image)
def get_href(self, element):
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace('\\','/')
def get_href(self, element: ET.Element) -> str:
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace("\\", "/")
def parse_embedded_svg(self, image, data):
def parse_embedded_svg(self, image: ET.Element, data: dict) -> ET.Element:
group = ET.Element("g")
group.attrib["transform"] = "translate({},{})".format(
self.convert_to_mm(image.attrib.get("x")), self.convert_to_mm(image.attrib.get("y"))
@@ -466,7 +476,7 @@ class SheetBuilder:
group.append(child)
return group
def change_titleblock(self, sheet, titleblock_name):
def change_titleblock(self, sheet: ifcopenshell.entity_instance, titleblock_name: str) -> None:
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
titleblock_path = tool.Drawing.get_default_titleblock_path(titleblock_name)
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -499,7 +509,7 @@ class SheetBuilder:
sheet_tree.write(sheet_path)
def convert_to_mm(self, value):
def convert_to_mm(self, value: str) -> float:
# CSS is what defines these possibilities
# https://www.w3.org/TR/SVG/refs.html#ref-css-values-3
# https://www.w3.org/TR/css-values-3/#absolute-lengths
@@ -520,5 +530,5 @@ class SheetBuilder:
return float(value[0:-2]) * (1 / 96) * 2.54 * 10
return float(value)
def mm_to_px(self, value):
def mm_to_px(self, value: float) -> float:
return (value / 25.4) * 96
@@ -726,7 +726,8 @@ class SvgWriter:
reference = tool.Drawing.get_drawing_reference(drawing)
if reference:
for sheet_reference in tool.Ifc.get().by_type("IfcDocumentReference"):
if sheet_reference.Description != "DRAWING" or sheet_reference.Location != reference.Location:
reference_description = tool.Drawing.get_reference_description(sheet_reference)
if reference_description != "DRAWING" or sheet_reference.Location != reference.Location:
continue
sheet = tool.Drawing.get_reference_document(sheet_reference)
if sheet:
@@ -538,7 +538,7 @@ class Helper:
)
position = None
if self.file.schema == "IFC2X3":
position = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint([0.0, 0.0, 0.0]))
position = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint([0.0, 0.0]))
curve = self.file.createIfcRectangleProfileDef("AREA", None, position, xdim, ydim)
return {"curve_ucs": curve_ucs, "curve": curve}
@@ -843,7 +843,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
# Recreate decompositions
tool.Root.recreate_decompositions(decomposition_relationships, old_to_new)
OverrideDuplicateMove.handle_linked_aggregates(old_to_new)
OverrideDuplicateMove.remove_linked_aggregate_data(old_to_new)
blenderbim.bim.handler.refresh_ui_data()
return old_to_new
@@ -896,25 +896,21 @@ class OverrideDuplicateMove(bpy.types.Operator):
if entity in old_to_new.keys():
core.remove_connection(tool.Geometry, connection=connection)
@staticmethod
def handle_linked_aggregates(old_to_new):
def remove_linked_aggregate_data(old_to_new):
for old, new in old_to_new.items():
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
if pset:
old_aggregate = ifcopenshell.util.element.get_aggregate(old)
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
if old_aggregate == new_aggregate:
parts = ifcopenshell.util.element.get_parts(new_aggregate)
if parts:
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
index += 1
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Index": index},
)
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], product=new[0])
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
@@ -940,6 +936,7 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
bl_description = "Create a new linked aggregate"
bl_idname = "bim.object_duplicate_move_linked_aggregate_macro"
bl_label = "IFC Duplicate Linked Aggregate"
bl_options = {"REGISTER", "UNDO"}
@@ -974,16 +971,16 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
obj.select_set(True)
parts = ifcopenshell.util.element.get_parts(element)
if parts:
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
index = get_max_index(parts)
add_linked_aggregate_pset(element, index)
index += 1
for part in parts:
if part.is_a("IfcElementAssembly"):
select_objects_and_add_data(part)
else:
add_linked_aggregate_pset(part, index)
index += 1
index = add_linked_aggregate_pset(part, index)
# index += 1
obj = tool.Ifc.get_object(part)
obj.select_set(True)
@@ -999,6 +996,8 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
pset=pset,
properties={"Index": index},
)
index += 1
else:
pass
@@ -1041,6 +1040,40 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if re.findall(pattern2, new_obj.name):
split_name = new_obj.name.split(".")
new_obj.name = split_name[0] + "_" + number
def get_max_index(parts):
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i['Index'] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
else:
return 0
def copy_linked_aggregate_data(old_to_new):
for old, new in old_to_new.items():
pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate")
if pset:
new_pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=new[0], name=self.pset_name
)
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=new_pset,
properties={"Index": pset["Index"]},
)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(old, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.assign_group", group=linked_aggregate_group[0], products=new)
if len(context.selected_objects) != 1:
return {"FINISHED"}
@@ -1061,28 +1094,17 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
select_objects_and_add_data(selected_element)
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
tool.Root.recreate_aggregate(old_to_new)
copy_linked_aggregate_data(old_to_new)
custom_incremental_naming_for_element_assembly(old_to_new)
# Recreate aggregate relationship
for old in old_to_new.keys():
if old.is_a("IfcElementAssembly"):
tool.Root.recreate_aggregate(old_to_new)
blenderbim.bim.handler.refresh_ui_data()
return old_to_new
@staticmethod
def get_max_index(parts):
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i["Index"] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
else:
return 0
class RefreshLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.refresh_linked_aggregate"
@@ -1216,6 +1238,23 @@ class RefreshLinkedAggregate(bpy.types.Operator):
return list(set(linked_aggregate_groups)), selected_parents
def get_original_matrix(element, base_instance):
selected_obj = tool.Ifc.get_object(base_instance)
selected_matrix = selected_obj.matrix_world
object_duplicate = tool.Ifc.get_object(element)
duplicate_matrix = object_duplicate.matrix_world.decompose()
return selected_matrix, duplicate_matrix
def set_new_matrix(selected_matrix, duplicate_matrix, old_to_new):
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
new_obj_matrix = new_base_matrix @ matrix_diff
new_obj.matrix_world = new_obj_matrix
active_element = tool.Ifc.get_entity(context.active_object)
if not active_element:
self.report({"INFO"}, "Object has no Ifc metadata.")
@@ -1255,10 +1294,7 @@ class RefreshLinkedAggregate(bpy.types.Operator):
element_aggregate = ifcopenshell.util.element.get_aggregate(element)
selected_obj = tool.Ifc.get_object(base_instance)
selected_matrix = selected_obj.matrix_world
object_duplicate = tool.Ifc.get_object(element)
duplicate_matrix = object_duplicate.matrix_world.decompose()
selected_matrix, duplicate_matrix = get_original_matrix(element, base_instance)
original_names = get_original_names(element)
@@ -1269,15 +1305,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
tool.Ifc.get_object(base_instance).select_set(True)
old_to_new = DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(
self, context
)
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
new_obj_matrix = new_base_matrix @ matrix_diff
new_obj.matrix_world = new_obj_matrix
old_to_new = DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context)
set_new_matrix(selected_matrix, duplicate_matrix, old_to_new)
for old, new in old_to_new.items():
if element_aggregate and new[0].is_a("IfcElementAssembly"):
@@ -24,6 +24,7 @@ classes = (
operator.AddLayer,
operator.AddListItem,
operator.AddMaterial,
operator.DuplicateMaterial,
operator.AddMaterialSet,
operator.AddProfile,
operator.AssignMaterial,
@@ -134,6 +134,19 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
material_prop_purge()
class DuplicateMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_material"
bl_label = "Diplicate Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty(name="Material ID")
def _execute(self, context):
ifc_file = tool.Ifc.get()
tool.Material.duplicate_material(ifc_file.by_id(self.material))
material_prop_purge()
bpy.ops.bim.load_materials()
class AddMaterialSet(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_material_set"
bl_label = "Add Material Set"
@@ -61,6 +61,8 @@ class BIM_PT_materials(Panel):
if self.props.materials and self.props.active_material_index < len(self.props.materials):
material = self.props.materials[self.props.active_material_index]
if material.ifc_definition_id:
op = row.operator("bim.duplicate_material", text="", icon="DUPLICATE")
op.material = material.ifc_definition_id
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
op.material = material.ifc_definition_id
op = row.operator("bim.enable_editing_material", text="", icon="GREASEPENCIL")
@@ -32,7 +32,9 @@ class AddInstanceFlooringCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operato
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
return tool.Ifc.get_entity(collection_obj)
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
return tool.Ifc.get_entity(collection_obj) and relating_type == "FLOORING"
def _execute(self, context):
@@ -61,7 +63,9 @@ class AddInstanceCeilingCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operator
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
return tool.Ifc.get_entity(collection_obj)
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
return tool.Ifc.get_entity(collection_obj) and relating_type == "CEILING"
def _execute(self, context):
@@ -116,9 +120,11 @@ class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operato
def poll(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
if element:
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
return context.selected_objects
return context.selected_objects and relating_type == "FLOORING"
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
@@ -147,7 +153,7 @@ class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operato
class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_instance_ceiling_coverings_from_walls"
bl_label = "Add Ceilings From Walls"
bl_label = "Add Ceiling From Walls"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add instance ceiling coverings from selected walls. The active object must be a wall and layered vertically"
@@ -155,9 +161,11 @@ class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator
def poll(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
if element:
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
return context.selected_objects
return context.selected_objects and relating_type == "CEILING"
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
@@ -27,7 +27,7 @@ import blenderbim.core.type
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_space"
bl_label = "Generate Space"
bl_label = "Generate Space from Cursor"
bl_options = {"REGISTER"}
bl_description = (
"Create a space from the cursor position. "
@@ -35,30 +35,34 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
"select the right space collection and run the operator"
)
@classmethod
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
active_obj = context.active_object
element = tool.Ifc.get_entity(active_obj)
return tool.Ifc.get_entity(collection_obj) and not element.is_a("IfcWall")
# @classmethod
# def poll(cls, context):
# print(context)
# collection = context.view_layer.active_layer_collection.collection
# collection_obj = collection.BIMCollectionProperties.obj
# active_obj = context.active_object
# element = tool.Ifc.get_entity(active_obj)
# return tool.Ifc.get_entity(collection_obj) and not element.is_a("IfcWall")
def _execute(self, context):
# This works as a 2.5 extruded polygon based on a cutting plane. Note
# that rooms exclude walls (i.e. not to wall midpoint or exterior /
# exterior edge.
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
def msg_no_collection(self, context):
self.layout.label(text="NO ACTIVE COLLECTION. PLEASE SELECT A SPATIAL COLLECTION OBJECT OR A WALL")
def msg_no_active_storey(self, context):
self.layout.label(text="NO ACTIVE STOREY. PLEASE SELECT A SPATIAL COLLECTION OBJECT")
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
if not collection_obj:
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
context.window_manager.popup_menu(msg_no_collection, title="Error", icon="ERROR")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
if not spatial_element or not spatial_element.is_a("IfcBuildingStorey"):
context.window_manager.popup_menu(msg_no_active_storey, title="Error", icon="ERROR")
return
core.generate_space(tool.Ifc, tool.Spatial, tool.Model, tool.Type)
@@ -70,12 +74,12 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Generate spaces from selected walls. The active object must be a wall"
@classmethod
def poll(cls, context):
active_obj = context.active_object
element = tool.Ifc.get_entity(active_obj)
if element:
return context.selected_objects and element.is_a("IfcWall")
# @classmethod
# def poll(cls, context):
# active_obj = context.active_object
# element = tool.Ifc.get_entity(active_obj)
# if element:
# return context.selected_objects and element.is_a("IfcWall")
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
@@ -87,19 +91,30 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(active_obj)
container = tool.Spatial.get_container(element)
def msg_no_active_object(self, context):
self.layout.label(text="No active object. Please select a wall")
def msg_no_active_wall(self, context):
self.layout.label(text="The active object is not a wall. Please select a wall.")
def msg_no_container(self, context):
self.layout.label(text="The wall is not contained. Please the selected wall in a building container")
def msg_no_selected_objects(self, context):
self.layout.label(text="No selected objects found. Please select walls.")
if not active_obj:
self.report({"ERROR"}, "No active object. Please select a wall")
context.window_manager.popup_menu(msg_no_active_object, title="Error", icon="ERROR")
return
element = tool.Ifc.get_entity(active_obj)
if element and not element.is_a("IfcWall"):
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
context.window_manager.popup_menu(msg_no_active_wall, title="Error", icon="ERROR")
return
if not container:
self.report({"ERROR"}, "The wall is not contained.")
context.window_manager.popup_menu(msg_no_container, title="Error", icon="ERROR")
return
if not context.selected_objects:
self.report({"ERROR"}, "No selected objects found. Please select walls.")
context.window_manager.popup_menu(msg_no_selected_objects, title="Error", icon="ERROR")
return
core.generate_spaces_from_walls(tool.Ifc, tool.Spatial, tool.Collector)
@@ -21,6 +21,7 @@ from . import ui, prop, operator, data
classes = (
operator.AddProfileDef,
operator.DuplicateProfileDef,
operator.DisableEditingArbitraryProfile,
operator.DisableEditingProfile,
operator.DisableProfileEditingUI,
@@ -66,9 +66,15 @@ class RemoveProfileDef(bpy.types.Operator, tool.Ifc.Operator):
profile: bpy.props.IntProperty()
def _execute(self, context):
props = context.scene.BIMProfileProperties
current_index = props.active_profile_index
ifcopenshell.api.run("profile.remove_profile", tool.Ifc.get(), profile=tool.Ifc.get().by_id(self.profile))
bpy.ops.bim.load_profiles()
# preserve selected index if possible
if props.profiles:
props.active_profile_index = min(current_index, len(props.profiles) - 1)
class EnableEditingProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_profile"
@@ -126,6 +132,27 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.load_profiles()
class DuplicateProfileDef(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_profile_def"
bl_label = "Duplicate Profile"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
props = context.scene.BIMProfileProperties
if len(props.profiles) > props.active_profile_index:
return True
cls.poll_message_set("No profile selected to duplicate.")
return False
def _execute(self, context):
props = context.scene.BIMProfileProperties
ifc_file = tool.Ifc.get()
profile = ifc_file.by_id(props.profiles[props.active_profile_index].ifc_definition_id)
tool.Profile.duplicate_profile(profile)
bpy.ops.bim.load_profiles()
class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_arbitrary_profile"
bl_label = "Enable Editing Arbitrary Profile"
@@ -70,6 +70,7 @@ class BIM_PT_profiles(Panel):
row = self.layout.row(align=True)
row.prop(self.props, "profile_classes", text="")
row.operator("bim.add_profile_def", text="", icon="ADD")
row.operator("bim.duplicate_profile_def", icon="DUPLICATE", text="")
self.layout.template_list(
"BIM_UL_profiles",
@@ -30,6 +30,7 @@ classes = (
operator.DisableAddingPresentationStyle,
operator.DisableEditingStyle,
operator.DisableEditingStyles,
operator.DuplicateStyle,
operator.EditStyle,
operator.EditSurfaceStyle,
operator.EnableAddingPresentationStyle,
@@ -493,6 +493,22 @@ class EnableAddingPresentationStyle(bpy.types.Operator, tool.Ifc.Operator):
props.is_adding = True
class DuplicateStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_style"
bl_label = "Duplicate Style"
bl_options = {"REGISTER", "UNDO"}
style: bpy.props.IntProperty(name="Style ID")
def _execute(self, context):
style_type = context.scene.BIMStylesProperties.style_type
ifc_file = tool.Ifc.get()
style = ifc_file.by_id(self.style)
tool.Style.duplicate_style(style)
bpy.ops.bim.disable_editing_styles()
bpy.ops.bim.load_styles(style_type=style_type)
class DisableAddingPresentationStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_adding_presentation_style"
bl_label = "Disable Add Presentation Style"
@@ -44,19 +44,33 @@ class BIM_PT_styles(Panel):
self.props = context.scene.BIMStylesProperties
if self.props.is_editing:
row = self.layout.row(align=True)
row.label(text="{} {}s".format(len(self.props.styles), self.props.style_type), icon="SHADING_RENDERED")
if not self.props.is_adding:
row.operator("bim.enable_adding_presentation_style", text="", icon="ADD")
row.operator("bim.disable_editing_styles", text="", icon="CANCEL")
else:
if not self.props.is_editing:
row = self.layout.row(align=True)
row.label(text="{} Styles".format(StylesData.data["total_styles"]), icon="SHADING_RENDERED")
blenderbim.bim.helper.prop_with_search(row, self.props, "style_type", text="")
row.operator("bim.load_styles", text="", icon="IMPORT").style_type = self.props.style_type
return
active_style = self.props.styles and self.props.active_style_index < len(self.props.styles)
row = self.layout.row(align=True)
row.label(text="{} {}s".format(len(self.props.styles), self.props.style_type), icon="SHADING_RENDERED")
row.operator("bim.disable_editing_styles", text="", icon="CANCEL")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
if not self.props.is_adding:
row.operator("bim.enable_adding_presentation_style", text="", icon="ADD")
if active_style:
style = self.props.styles[self.props.active_style_index]
material_name = StylesData.data["styles_to_blender_material_names"][self.props.active_style_index]
material = bpy.data.materials[material_name]
row.operator("bim.duplicate_style", text="", icon="DUPLICATE").style = style.ifc_definition_id
row.operator("bim.select_by_style", text="", icon="RESTRICT_SELECT_OFF").style = style.ifc_definition_id
op = row.operator("bim.enable_editing_style", text="", icon="GREASEPENCIL")
op.style = style.ifc_definition_id
row.operator("bim.remove_style", text="", icon="X").style = style.ifc_definition_id
self.layout.template_list("BIM_UL_styles", "", self.props, "styles", self.props, "active_style_index")
# adding a new IfcSurfaceStyle
@@ -77,17 +91,7 @@ class BIM_PT_styles(Panel):
row.operator("bim.disable_adding_presentation_style", text="", icon="CANCEL")
# style ui tools
if self.props.styles and self.props.active_style_index < len(self.props.styles):
row = self.layout.row(align=True)
style = self.props.styles[self.props.active_style_index]
material_name = StylesData.data["styles_to_blender_material_names"][self.props.active_style_index]
material = bpy.data.materials[material_name]
op = row.operator("bim.enable_editing_style", text="Edit Style", icon="GREASEPENCIL")
op.style = style.ifc_definition_id
row.operator("bim.select_by_style", text="", icon="RESTRICT_SELECT_OFF").style = style.ifc_definition_id
row.operator("bim.remove_style", text="", icon="X").style = style.ifc_definition_id
if active_style:
row = self.layout.row(align=True)
row.prop(material.BIMStyleProperties, "active_style_type", icon="SHADING_RENDERED", text="")
op = row.operator("bim.update_current_style", icon="FILE_REFRESH", text="")
+5 -4
View File
@@ -46,7 +46,8 @@ def add_instance_flooring_covering_from_cursor(ifc, spatial, model, Type, geomet
obj = spatial.get_named_obj_from_mesh(name, mesh)
spatial.set_obj_origin_to_cursor_position(obj)
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.traslate_obj_to_z_location(obj, z)
spatial.link_obj_to_active_collection(obj)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
@@ -74,7 +75,7 @@ def add_instance_ceiling_covering_from_cursor(ifc, spatial, model, Type, geometr
else:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
z = covering.get_z_from_ceiling_height()
ceiling_height = covering.get_z_from_ceiling_height()
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
@@ -87,8 +88,8 @@ def add_instance_ceiling_covering_from_cursor(ifc, spatial, model, Type, geometr
obj = spatial.get_named_obj_from_mesh(name, mesh)
spatial.set_obj_origin_to_cursor_position(obj)
spatial.traslate_obj_to_z_location(obj, z)
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.traslate_obj_to_z_location(obj, z+ceiling_height)
spatial.link_obj_to_active_collection(obj)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
+26 -14
View File
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from pathlib import Path
import ifcopenshell
def enable_editing_text(drawing, obj=None):
@@ -66,7 +67,7 @@ def disable_editing_sheets(drawing):
drawing.disable_editing_sheets()
def add_sheet(ifc, drawing, titleblock=None):
def add_sheet(ifc, drawing, titleblock: ifcopenshell.entity_instance):
sheet = ifc.run("document.add_information")
layout = ifc.run("document.add_reference", information=sheet)
titleblock_reference = ifc.run("document.add_reference", information=sheet)
@@ -77,16 +78,17 @@ def add_sheet(ifc, drawing, titleblock=None):
else:
attributes = {"Identification": identification, "Name": "UNTITLED", "Scope": "SHEET"}
ifc.run("document.edit_information", information=sheet, attributes=attributes)
ifc.run(
"document.edit_reference",
reference=layout,
attributes={"Location": drawing.get_default_layout_path(identification, "UNTITLED"), "Description": "LAYOUT"},
attributes = drawing.generate_reference_attributes(
layout, Location=drawing.get_default_layout_path(identification, "UNTITLED"), Description="LAYOUT"
)
ifc.run(
"document.edit_reference",
reference=titleblock_reference,
attributes={"Location": drawing.get_default_titleblock_path(titleblock), "Description": "TITLEBLOCK"},
ifc.run("document.edit_reference", reference=layout, attributes=attributes)
attributes = drawing.generate_reference_attributes(
layout, Location=drawing.get_default_titleblock_path(titleblock), Description="TITLEBLOCK"
)
ifc.run("document.edit_reference", reference=titleblock_reference, attributes=attributes)
drawing.create_svg_sheet(sheet, titleblock)
drawing.import_sheets()
@@ -94,7 +96,12 @@ def add_sheet(ifc, drawing, titleblock=None):
def regenerate_sheet(drawing, sheet=None):
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
drawing.create_svg_sheet(sheet, drawing.sanitise_filename(Path(titleblock_uri).stem))
drawing.add_drawings(sheet)
try:
drawing.add_drawings(sheet)
except FileNotFoundError:
path_layout = drawing.get_document_uri(sheet, "LAYOUT")
if drawing.does_file_exist(path_layout):
drawing.delete_file(path_layout)
def open_sheet(drawing, sheet=None):
@@ -111,8 +118,12 @@ def remove_sheet(ifc, drawing, sheet=None):
drawing.import_sheets()
def rename_sheet(ifc, drawing, sheet=None, identification=None, name=None):
ifc.run("document.edit_information", information=sheet, attributes={"Identification": identification, "Name": name})
def rename_sheet(ifc, drawing, sheet: ifcopenshell.entity_instance, identification: str, name: str) -> None:
if ifc.get_schema() == "IFC2X3":
attributes = {"DocumentId": identification, "Name": name}
else:
attributes = {"Identification": identification, "Name": name}
ifc.run("document.edit_information", information=sheet, attributes=attributes)
for reference in drawing.get_document_references(sheet):
description = drawing.get_reference_description(reference)
if description == "SHEET":
@@ -133,8 +144,9 @@ def rename_sheet(ifc, drawing, sheet=None, identification=None, name=None):
drawing.move_file(old_location, ifc.resolve_uri(new_location))
def rename_reference(ifc, reference=None, identification=None):
ifc.run("document.edit_reference", reference=reference, attributes={"Identification": identification})
def rename_reference(ifc, drawing, reference=None, identification=None):
attributes = drawing.generate_reference_attributes(reference, Identification=identification)
ifc.run("document.edit_reference", reference=reference, attributes=attributes)
def load_schedules(drawing):
+3 -2
View File
@@ -189,7 +189,8 @@ def generate_space(ifc, spatial, model, Type):
name = "Space"
obj = spatial.get_named_obj_from_mesh(name, mesh)
spatial.set_obj_origin_to_cursor_position(obj)
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.traslate_obj_to_z_location(obj, z)
spatial.link_obj_to_active_collection(obj)
spatial.assign_ifcspace_class_to_obj(obj)
@@ -214,7 +215,7 @@ def generate_spaces_from_walls(ifc, spatial, collector):
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
spatial.set_obj_origin_to_bboxcenter(obj)
spatial.set_obj_origin_to_bboxcenter_and_zero_elevation(obj)
spatial.traslate_obj_to_z_location(obj, z)
spatial.link_obj_to_active_collection(obj)
+3 -1
View File
@@ -329,6 +329,7 @@ class Drawing:
def get_name(cls, element): pass
def get_path_filename(cls, uri): pass
def get_reference_description(cls, reference): pass
def generate_reference_attributes(cls, reference, **attributes): pass
def get_reference_document(cls, reference): pass
def get_reference_location(cls, reference): pass
def get_references_with_location(cls, location): pass
@@ -871,7 +872,8 @@ class Spatial:
def get_transformed_mesh_from_local_to_global(cls, mesh): pass
def edit_active_space_obj_from_mesh(cls, mesh): pass
def set_obj_origin_to_bboxcenter(cls, obj): pass
def set_obj_origin_to_cursor_position(cls, obj): pass
def set_obj_origin_to_bboxcenter_and_zero_elevation(cls, obj): pass
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj): pass
def get_selected_objects(cls): pass
def get_active_obj(cls): pass
def get_active_obj_z(cls): pass
@@ -187,6 +187,9 @@ class Collector(blenderbim.core.tool.Collector):
if any(e for e in element.IsNestedBy[0].RelatedObjects if not e.is_a("IfcPort")):
return cls._create_own_collection(obj)
if getattr(element, "HasSurfaceFeatures", None):
return cls._create_own_collection(obj)
@classmethod
def _get_collection(cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> bpy.types.Collection:
"""get or create collection for the element based on it's type"""
@@ -247,6 +250,13 @@ class Collector(blenderbim.core.tool.Collector):
if collection:
return collection
if element.is_a("IfcSurfaceFeature") and element.file.schema == "IFC4X3":
adherend = element.AdheresToElement[0].RelatingElement
adherend_obj = tool.Ifc.get_object(adherend)
collection = adherend_obj.BIMObjectProperties.collection
if collection:
return collection
if element.is_a("IfcProject"):
return bpy.context.scene.collection
+51 -16
View File
@@ -240,7 +240,7 @@ class Drawing(blenderbim.core.tool.Drawing):
)
@classmethod
def create_svg_sheet(cls, document, titleblock):
def create_svg_sheet(cls, document: ifcopenshell.entity_instance, titleblock: str) -> str:
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir
uri = cls.get_document_uri(document, "LAYOUT")
@@ -251,16 +251,16 @@ class Drawing(blenderbim.core.tool.Drawing):
def add_drawings(cls, sheet):
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir
sheet_reference = None
drawing_references = {}
drawing_names = []
for reference in cls.get_document_references(sheet):
if reference.Description == "LAYOUT":
sheet_reference = reference
elif reference.Description == "DRAWING":
reference_description = cls.get_reference_description(reference)
if reference_description == "DRAWING":
drawing_references[Path(reference.Location).stem] = reference
drawing_names.append(Path(reference.Location).stem)
for annotation in [e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"]:
if annotation.Name in drawing_names:
sheet_builder.add_drawing(sheet_reference, annotation, sheet)
for drawing_annotation in [e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"]:
if drawing_annotation.Name in drawing_names:
sheet_builder.add_drawing(drawing_references[drawing_annotation.Name], drawing_annotation, sheet)
@classmethod
def delete_collection(cls, collection):
@@ -421,7 +421,7 @@ class Drawing(blenderbim.core.tool.Drawing):
else:
references = document.HasDocumentReferences
for reference in references:
if description and reference.Description != description:
if description and cls.get_reference_description(reference) != description:
continue
location = cls.get_document_uri(reference)
if location:
@@ -823,7 +823,8 @@ class Drawing(blenderbim.core.tool.Drawing):
continue
for reference in cls.get_document_references(sheet):
if reference.Description in ("SHEET", "LAYOUT", "RASTER"):
reference_description = cls.get_reference_description(reference)
if reference_description in ("SHEET", "LAYOUT", "RASTER"):
# These references are an internal detail and should not be visible to users
continue
new = props.sheets.add()
@@ -836,7 +837,7 @@ class Drawing(blenderbim.core.tool.Drawing):
new.identification = reference.Identification or ""
new.name = os.path.basename(reference.Location)
new.reference_type = reference.Description
new.reference_type = reference_description
@classmethod
def get_active_sheet(cls, context):
@@ -1568,9 +1569,28 @@ class Drawing(blenderbim.core.tool.Drawing):
tree.write(uri, pretty_print=True, xml_declaration=True, encoding="utf-8")
@classmethod
def get_reference_description(cls, reference):
def get_reference_description(cls, reference: ifcopenshell.entity_instance) -> Union[str, None]:
if reference.file.schema == "IFC2X3":
return reference.Name
return reference.Description
@classmethod
def generate_reference_attributes(cls, reference: ifcopenshell.entity_instance, **attributes: Any) -> dict[str, Any]:
"""will automatically convert attributes below for IFC2X3 compatibility:
- Identification -> ItemReference
- Description -> Name
"""
if reference.file.schema == "IFC2X3":
if "Description" in attributes:
attributes["Name"] = attributes["Description"]
del attributes["Description"]
if "Identification" in attributes:
attributes["ItemReference"] = attributes["Identification"]
del attributes["Identification"]
return attributes
@classmethod
def get_reference_location(cls, reference):
return reference.Location
@@ -1637,8 +1657,23 @@ class Drawing(blenderbim.core.tool.Drawing):
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"}
# exclude annotations to avoid including annotations from other drawings
elements = {i for i in elements if not i.is_a("IfcAnnotation")}
updated_set = set()
for i in elements:
# exclude annotations to avoid including annotations from other drawings
if not i.is_a("IfcAnnotation"):
updated_set.add(i)
#add aggregate too, if element is host by one
if i.Decomposes:
aggregate = i.Decomposes[0].RelatingObject
#remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615
if not aggregate.is_a("IfcProject"):
updated_set.add(aggregate)
# After the iteration is complete, update elements with updated set
elements.update(updated_set)
# add annotations from the current drawing
annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing))
elements.update(annotations)
@@ -1816,8 +1851,8 @@ class Drawing(blenderbim.core.tool.Drawing):
has_context = True
break
# Don't hide IfcAnnotations as some of them might exist without representations
if has_context or element.is_a("IfcAnnotation"):
# Don't hide IfcAnnotations or Aggregates as some of them might exist without representations
if has_context or element.is_a("IfcAnnotation") or element.IsDecomposedBy:
element_obj_names.add(obj.name)
# Note that render visibility is only set on drawing generation time for speed.
+1 -1
View File
@@ -622,7 +622,7 @@ class Geometry(blenderbim.core.tool.Geometry):
obj.data.BIMMeshProperties.material_checksum = str([s.id() for s in cls.get_styles(obj) if s])
@classmethod
def record_object_position(cls, obj):
def record_object_position(cls, obj: bpy.types.Object) -> None:
# These are recorded separately because they have different numerical tolerances
obj.BIMObjectProperties.location_checksum = repr(np.array(obj.matrix_world.translation).tobytes())
obj.BIMObjectProperties.rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes())
@@ -38,6 +38,12 @@ class Material(blenderbim.core.tool.Material):
def disable_editing_materials(cls):
bpy.context.scene.BIMMaterialProperties.is_editing = False
@classmethod
def duplicate_material(cls, material: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
new_material = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), material)
new_material.Name = material.Name + "_copy"
return new_material
@classmethod
def enable_editing_materials(cls):
bpy.context.scene.BIMMaterialProperties.is_editing = True
+3 -1
View File
@@ -19,6 +19,8 @@
import bpy
import blenderbim.core.tool
import blenderbim.tool as tool
import ifcopenshell
from typing import Union
class Owner(blenderbim.core.tool.Owner):
@@ -27,7 +29,7 @@ class Owner(blenderbim.core.tool.Owner):
bpy.context.scene.BIMOwnerProperties.active_user_id = user.id()
@classmethod
def get_user(cls):
def get_user(cls) -> Union[ifcopenshell.entity_instance, None]:
if bpy.context.scene.BIMOwnerProperties.active_user_id:
return tool.Ifc.get().by_id(bpy.context.scene.BIMOwnerProperties.active_user_id)
elif tool.Ifc.get_schema() == "IFC2X3":
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.unit
import ifcopenshell.util.placement
import ifcopenshell.util.representation
@@ -75,3 +76,9 @@ class Profile(blenderbim.core.tool.Profile):
@classmethod
def get_model_profiles(cls):
return tool.Ifc.get().by_type("IfcProfileDef")
@classmethod
def duplicate_profile(cls, profile: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
new_profile = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), profile)
new_profile.ProfileName = profile.ProfileName + "_copy"
return new_profile
+20 -2
View File
@@ -583,14 +583,32 @@ class Spatial(blenderbim.core.tool.Spatial):
obj.location = newLoc
@classmethod
def set_obj_origin_to_cursor_position(cls, obj):
def set_obj_origin_to_bboxcenter_and_zero_elevation(cls, obj):
mat = obj.matrix_world
inverted = mat.inverted()
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
global_bbox_center = mat @ local_bbox_center
global_obj_origin = global_bbox_center
global_obj_origin.z = 0
oldLoc = obj.location
newLoc = global_obj_origin
diff = newLoc - oldLoc
for vert in obj.data.vertices:
aux_vector = mat @ vert.co
aux_vector = aux_vector - diff
vert.co = inverted @ aux_vector
obj.location = newLoc
@classmethod
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj):
mat = obj.matrix_world
inverted = mat.inverted()
collection = bpy.context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
x, y = bpy.context.scene.cursor.location.xy
z = collection_obj.matrix_world.translation.z
z = 0
oldLoc = obj.location
newLoc = Vector((x, y, z))
+7
View File
@@ -19,6 +19,7 @@
import bpy
import numpy as np
import ifcopenshell
import ifcopenshell.util.element
import blenderbim.core.tool
import blenderbim.tool as tool
import blenderbim.bim.helper
@@ -59,6 +60,12 @@ class Style(blenderbim.core.tool.Style):
def disable_editing_styles(cls):
bpy.context.scene.BIMStylesProperties.is_editing = False
@classmethod
def duplicate_style(cls, style: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
new_style = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), style)
new_style.Name = style.Name + "_copy"
return new_style
@classmethod
def enable_editing(cls, obj):
obj.BIMStyleProperties.is_editing = True
@@ -103,6 +103,7 @@ For Linux or Mac:
# Remove and link other IfcOpenShell utilities
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifccsv.py
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcdiff.py
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/bsdd.py
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifc4d
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifc5d
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifccityjson
@@ -110,9 +111,11 @@ For Linux or Mac:
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcpatch
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifctester
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcfm
$ rm -r $BLENDER_ADDON_PATH/libs/Desktop
$ ln -s $PWD/src/ifccsv/ifccsv.py $BLENDER_ADDON_PATH/libs/site/packages/ifccsv.py
$ ln -s $PWD/src/ifcdiff/ifcdiff.py $BLENDER_ADDON_PATH/libs/site/packages/ifcdiff.py
$ ln -s $PWD/src/bsdd/bsdd.py $BLENDER_ADDON_PATH/libs/site/packages/bsdd.py
$ ln -s $PWD/src/ifc4d/ifc4d $BLENDER_ADDON_PATH/libs/site/packages/ifc4d
$ ln -s $PWD/src/ifc5d/ifc5d $BLENDER_ADDON_PATH/libs/site/packages/ifc5d
$ ln -s $PWD/src/ifccityjson/ifccityjson $BLENDER_ADDON_PATH/libs/site/packages/ifccityjson
@@ -120,6 +123,7 @@ For Linux or Mac:
$ ln -s $PWD/src/ifcpatch/ifcpatch $BLENDER_ADDON_PATH/libs/site/packages/ifcpatch
$ ln -s $PWD/src/ifctester/ifctester $BLENDER_ADDON_PATH/libs/site/packages/ifctester
$ ln -s $PWD/src/ifcfm/ifcfm $BLENDER_ADDON_PATH/libs/site/packages/ifcfm
$ ln -s $PWD/src/blenderbim/blenderbim/libs/desktop $BLENDER_ADDON_PATH/libs/Desktop
# Manually download some third party dependencies
$ cd $BLENDER_ADDON_PATH/bim/data/gantt
@@ -168,6 +172,7 @@ Before running it follow the instructions descibed after `rem` tags.
echo Remove and link other IfcOpenShell utilities...
del "%blenderbim%\libs\site\packages\ifccsv.py"
del "%blenderbim%\libs\site\packages\ifcdiff.py"
del "%blenderbim%\libs\site\packages\bsdd.py"
rd /S /Q "%blenderbim%\libs\site\packages\ifc4d"
rd /S /Q "%blenderbim%\libs\site\packages\ifc5d"
rd /S /Q "%blenderbim%\libs\site\packages\ifccityjson"
@@ -175,9 +180,11 @@ Before running it follow the instructions descibed after `rem` tags.
rd /S /Q "%blenderbim%\libs\site\packages\ifcpatch"
rd /S /Q "%blenderbim%\libs\site\packages\ifctester"
rd /S /Q "%blenderbim%\libs\site\packages\ifcfm"
rd /S /Q "%blenderbim%\libs\desktop"
mklink "%blenderbim%\libs\site\packages\ifccsv.py" "%cd%\src\ifccsv\ifccsv.py"
mklink "%blenderbim%\libs\site\packages\ifcdiff.py" "%cd%\src\ifcdiff\ifcdiff.py"
mklink "%blenderbim%\libs\site\packages\bsdd.py" "%cd%\src\bsdd\bsdd.py"
mklink /D "%blenderbim%\libs\site\packages\ifc4d" "%cd%\src\ifc4d\ifc4d"
mklink /D "%blenderbim%\libs\site\packages\ifc5d" "%cd%\src\ifc5d\ifc5d"
mklink /D "%blenderbim%\libs\site\packages\ifccityjson" "%cd%\src\ifccityjson\ifccityjson"
@@ -185,6 +192,7 @@ Before running it follow the instructions descibed after `rem` tags.
mklink /D "%blenderbim%\libs\site\packages\ifcpatch" "%cd%\src\ifcpatch\ifcpatch"
mklink /D "%blenderbim%\libs\site\packages\ifctester" "%cd%\src\ifctester\ifctester"
mklink /D "%blenderbim%\libs\site\packages\ifcfm" "%cd%\src\ifcfm\ifcfm"
mklink /D "%blenderbim%\libs\desktop" "%cd%\src\blenderbim\blenderbim\libs\desktop"
echo Manually downloading some third party dependencies...
curl https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js -o "%blenderbim%\bim\data\gantt\jsgantt.js"
+9 -8
View File
@@ -19,6 +19,7 @@
import json
import pytest
import blenderbim.core.tool
from typing import Any, Self, Optional
@pytest.fixture
@@ -241,12 +242,12 @@ def voider():
class Prophecy:
def __init__(self, cls):
self.subject = cls
self.predictions = []
self.calls = []
self.return_values = {}
self.should_call = None
self.predictions: list[dict] = []
self.calls: list[dict] = []
self.return_values: dict[str, Any] = {}
self.should_call: Optional[dict] = None
def __getattr__(self, attr):
def __getattr__(self, attr: str):
if not hasattr(self.subject, attr):
raise AttributeError(f"Prophecy {self.subject} has no attribute {attr}")
@@ -270,12 +271,12 @@ class Prophecy:
self.predictions.append({"type": "SHOULD_BE_CALLED", "number": number, "call": self.should_call})
return self
def will_return(self, value):
def will_return(self, value: Any) -> Self:
key = json.dumps(self.should_call, sort_keys=True)
self.return_values[key] = value
return self
def verify(self):
def verify(self) -> None:
predicted_calls = []
for prediction in self.predictions:
predicted_calls.append(prediction["call"])
@@ -285,7 +286,7 @@ class Prophecy:
if call not in predicted_calls:
raise Exception(f"Unpredicted call: {call}")
def verify_should_be_called(self, prediction):
def verify_should_be_called(self, prediction: dict) -> None:
if prediction["number"]:
count = self.calls.count(prediction["call"])
if count != prediction["number"]:
+19 -5
View File
@@ -95,15 +95,21 @@ class TestAddSheet:
information="sheet",
attributes={"Identification": "u_identification", "Name": "UNTITLED", "Scope": "SHEET"},
).should_be_called()
drawing.generate_reference_attributes(
"reference", Location="layout_path", Description="LAYOUT"
).should_be_called().will_return("attributes")
ifc.run(
"document.edit_reference",
reference="reference",
attributes={"Location": "layout_path", "Description": "LAYOUT"},
attributes="attributes",
).should_be_called()
drawing.generate_reference_attributes(
"reference", Location="titleblock_path", Description="TITLEBLOCK"
).should_be_called().will_return("attributes2")
ifc.run(
"document.edit_reference",
reference="reference",
attributes={"Location": "titleblock_path", "Description": "TITLEBLOCK"},
attributes="attributes2",
).should_be_called()
drawing.create_svg_sheet("sheet", "titleblock").should_be_called()
drawing.import_sheets().should_be_called()
@@ -122,15 +128,21 @@ class TestAddSheet:
information="sheet",
attributes={"DocumentId": "u_identification", "Name": "UNTITLED", "Scope": "SHEET"},
).should_be_called()
drawing.generate_reference_attributes(
"reference", Location="layout_path", Description="LAYOUT"
).should_be_called().will_return("attributes")
ifc.run(
"document.edit_reference",
reference="reference",
attributes={"Location": "layout_path", "Description": "LAYOUT"},
attributes="attributes",
).should_be_called()
drawing.generate_reference_attributes(
"reference", Location="titleblock_path", Description="TITLEBLOCK"
).should_be_called().will_return("attributes2")
ifc.run(
"document.edit_reference",
reference="reference",
attributes={"Location": "titleblock_path", "Description": "TITLEBLOCK"},
attributes="attributes2",
).should_be_called()
drawing.create_svg_sheet("sheet", "titleblock").should_be_called()
drawing.import_sheets().should_be_called()
@@ -490,7 +502,9 @@ class TestUpdateDrawingName:
)
ifc.resolve_uri("relative_layout_uri").should_be_called().will_return("absolute_layout_uri")
drawing.does_file_exist("absolute_layout_uri").should_be_called().will_return(True)
drawing.update_embedded_svg_location("absolute_layout_uri", "reference_with_old_location", "new_uri").should_be_called()
drawing.update_embedded_svg_location(
"absolute_layout_uri", "reference_with_old_location", "new_uri"
).should_be_called()
drawing.is_editing_sheets().should_be_called().will_return(True)
drawing.import_sheets().should_be_called()
@@ -62,7 +62,9 @@
#include <STEPConstruct_PointHasher.hxx>
#include "clash_utils.h"
#ifdef WITH_HDF5
#include "H5Cpp.h"
#endif
namespace IfcGeom {
@@ -1504,6 +1506,7 @@ namespace IfcGeom {
}
}
#ifdef WITH_HDF5
void write_h5() {
H5::H5File file("filename.h5", H5F_ACC_TRUNC);
H5::Group shapes = file.createGroup("/shapes");
@@ -1714,6 +1717,7 @@ namespace IfcGeom {
colours_dataset.write(flat_colours.data(), H5::PredType::NATIVE_FLOAT);
}
}
#endif
template <typename T>
void apply_matrix_to_flat_verts(const std::vector<T>& flat_list, const std::vector<T>& matrix, std::vector<T>& result) {
@@ -1776,9 +1780,11 @@ namespace IfcGeom {
aabb.Add(vs_transformed.back());
}
/*
std::cout << "aabb: ";
aabb.DumpJson(std::cout);
std::cout << std::endl;
*/
std::unordered_map<std::tuple<int, int, int>, std::vector<size_t>, boost::hash<std::tuple<int, int, int>>> quantized_normal_counts;
@@ -1863,9 +1869,11 @@ namespace IfcGeom {
obb.SetZComponent(ax3.Direction(), halfsize.Z());
obb.SetCenter(cent.Transformed(trsf2.Inverted()));
/*
std::cout << "obb: ";
obb.DumpJson(std::cout);
std::cout << std::endl;
*/
}
const auto& t = elem->product();
@@ -39,6 +39,7 @@ import sys
import tempfile
import zipfile
from pathlib import Path
from typing import Optional
import ifcopenshell.util.file
@@ -197,12 +198,14 @@ def register_schema(schema):
register_schema_attributes(schema.schema)
def schema_by_name(schema=None, schema_version=None):
def schema_by_name(
schema: Optional[str] = None, schema_version: Optional[tuple[int, ...]] = None
) -> ifcopenshell_wrapper.schema_definition:
"""Returns an object allowing you to query the IFC schema itself
:param schema: Which IFC schema to use, chosen from "IFC2X3", "IFC4",
or "IFC4X3". These refer to the ISO approved versions of IFC.
:type schema: string
:type schema: string, optional
:param schema_version: If you want to specify an exact version of IFC
that may not be an ISO approved version, use this argument instead
of ``schema``. IFC versions on technical.buildingsmart.org are
@@ -211,13 +214,15 @@ def schema_by_name(schema=None, schema_version=None):
ADD2 TC1, which is the official version approved by ISO when people
refer to "IFC4". Generally you should not use this argument unless
you are testing non-ISO IFC releases.
:type schema_version: tuple[int]
:type schema_version: tuple[int, ...], optional
:return: Schema definition object.
:rtype: ifocpenshell_wrapper.schema_definition
"""
if schema_version:
prefixes = ("IFC", "X", "_ADD", "_TC")
schema = "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, schema_version))
else:
schema = {"IFC4X3": "IFC4X3_ADD1"}.get(schema, schema)
schema = {"IFC4X3": "IFC4X3_ADD2"}.get(schema, schema)
return ifcopenshell_wrapper.schema_by_name(schema)
@@ -15,10 +15,17 @@
#
# 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, Optional
class Usecase:
def __init__(self, file, information=None, attributes=None):
def __init__(
self,
file: ifcopenshell.file,
information: ifcopenshell.entity_instance,
attributes: Optional[dict[str, Any]] = None,
):
"""Edits the attributes of an IfcDocumentInformation
For more information about the attributes and data types of an
@@ -44,6 +51,6 @@ class Usecase:
self.file = file
self.settings = {"information": information, "attributes": attributes or {}}
def execute(self):
def execute(self) -> None:
for name, value in self.settings["attributes"].items():
setattr(self.settings["information"], name, value)
@@ -15,10 +15,17 @@
#
# 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, Optional
class Usecase:
def __init__(self, file, reference=None, attributes=None):
def __init__(
self,
file: ifcopenshell.file,
reference: ifcopenshell.entity_instance,
attributes: Optional[dict[str, Any]] = None,
):
"""Edits the attributes of an IfcDocumentReference
For more information about the attributes and data types of an
@@ -47,6 +54,6 @@ class Usecase:
self.file = file
self.settings = {"reference": reference, "attributes": attributes or {}}
def execute(self):
def execute(self) -> None:
for name, value in self.settings["attributes"].items():
setattr(self.settings["reference"], name, value)
@@ -17,20 +17,34 @@
# 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]
class Usecase:
def __init__(self, file, **settings):
def __init__(
self,
file: ifcopenshell.file,
product: ifcopenshell.entity_instance,
matrix: Optional[NPArrayOfFloats] = None,
is_si=True,
should_transform_children=False,
):
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
self.settings = {
"product": product,
"matrix": matrix if matrix is not None else np.eye(4),
"is_si": is_si,
"should_transform_children": should_transform_children,
}
def execute(self):
def execute(self) -> ifcopenshell.entity_instance:
if not hasattr(self.settings["product"], "ObjectPlacement"):
return
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -69,34 +83,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 +133,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 +155,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
@@ -15,10 +15,11 @@
#
# 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, identification="APTR", name="Aperture Science"):
def __init__(self, file: ifcopenshell.file, identification: str = "APTR", name: str = "Aperture Science"):
"""Adds a new organisation
Organisations are the main way to identify manufacturers, suppliers, and
@@ -45,7 +46,7 @@ class Usecase:
self.file = file
self.settings = {"identification": identification, "name": name}
def execute(self):
def execute(self) -> ifcopenshell.entity_instance:
data = {"Name": self.settings["name"]}
if self.file.schema == "IFC2X3":
data["Id"] = self.settings["identification"]
@@ -15,10 +15,17 @@
#
# 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, identification="HSeldon", family_name="Seldon", given_name="Hari"):
def __init__(
self,
file: ifcopenshell.entity_instance,
identification: str = "HSeldon",
family_name: str = "Seldon",
given_name: str = "Hari",
):
"""Adds a new person
Persons are used to identify a legal or liable representative of an
@@ -48,7 +55,7 @@ class Usecase:
"given_name": given_name,
}
def execute(self):
def execute(self) ->ifcopenshell.entity_instance:
data = {"FamilyName": self.settings["family_name"], "GivenName": self.settings["given_name"]}
if self.file.schema == "IFC2X3":
data["Id"] = self.settings["identification"]
@@ -15,10 +15,16 @@
#
# 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, person=None, organisation=None):
def __init__(
self,
file: ifcopenshell.entity_instance,
person: ifcopenshell.entity_instance,
organisation: ifcopenshell.entity_instance,
):
"""Adds a paired person and organisation
A person and an organisation may be paired to create a representative
@@ -47,5 +53,5 @@ class Usecase:
self.file = file
self.settings = {"person": person, "organisation": organisation}
def execute(self):
def execute(self) -> ifcopenshell.entity_instance:
return self.file.createIfcPersonAndOrganization(self.settings["person"], self.settings["organisation"])
@@ -19,10 +19,11 @@
import time
import ifcopenshell
import ifcopenshell.api.owner.settings
from typing import Union
class Usecase:
def __init__(self, file):
def __init__(self, file: ifcopenshell.entity_instance):
"""Creates a new owner history indicating an element was added
Any object in IFC with a unique ID and name (such as physical products,
@@ -59,8 +60,9 @@ class Usecase:
are writing your own advanced scripts and want to take advantage of the
easier ownership tracking.
:return: The newly created IfcOwnerHistory element.
:rtype: ifcopenshell.entity_instance.entity_instance
:return: The newly created IfcOwnerHistory element or `None` if it's
not IFC2X3 and user or application is not found in the current project.
:rtype: Union[ifcopenshell.entity_instance.entity_instance, None]
Example:
@@ -99,7 +101,7 @@ class Usecase:
self.file = file
self.settings = {}
def execute(self):
def execute(self) -> Union[ifcopenshell.entity_instance, None]:
user = ifcopenshell.api.owner.settings.get_user(self.file)
if self.file.schema != "IFC2X3" and not user:
return
@@ -17,12 +17,19 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import datetime
import ifcopenshell.util.constraint
import ifcopenshell.util.date
import ifcopenshell.util.sequence
from typing import Any, Optional
class Usecase:
def __init__(self, file, task_time=None, attributes=None):
def __init__(
self,
file: ifcopenshell.file,
task_time: ifcopenshell.entity_instance,
attributes: Optional[dict[str, Any]] = None,
):
"""Edits the attributes of an IfcTaskTime
For more information about the attributes and data types of an
@@ -55,7 +62,7 @@ class Usecase:
self.file = file
self.settings = {"task_time": task_time, "attributes": attributes or {}}
def execute(self):
def execute(self) -> None:
self.task = self.get_task()
self.calendar = ifcopenshell.util.sequence.derive_calendar(self.task)
@@ -169,12 +176,12 @@ class Usecase:
duration, "IfcDuration"
)
def get_task(self):
return [
def get_task(self) -> ifcopenshell.entity_instance:
return next(
e
for e in self.file.get_inverse(self.settings["task_time"])
if e.is_a("IfcTask")
][0]
)
def handle_resource_calculation(self):
resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_deep=False)
@@ -17,10 +17,16 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.date
from typing import Any, Optional
class Usecase:
def __init__(self, file, work_time=None, attributes=None):
def __init__(
self,
file: ifcopenshell.file,
work_time: ifcopenshell.entity_instance,
attributes: Optional[dict[str, Any]] = None,
):
"""Edits the attributes of an IfcWorkTime
For more information about the attributes and data types of an
@@ -53,13 +59,15 @@ class Usecase:
self.file = file
self.settings = {"work_time": work_time, "attributes": attributes or {}}
def execute(self):
def execute(self) -> None:
for name, value in self.settings["attributes"].items():
if name in ("Start", "StartDate"):
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate")
# 4 IfcWorktime Start
self.settings["work_time"][4] = value
elif name in ("Finish", "FinishDate"):
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate")
# 5 IfcWorktime Finish
self.settings["work_time"][5] = value
else:
setattr(self.settings["work_time"], name, value)
@@ -18,10 +18,11 @@
import ifcopenshell
import ifcopenshell.util.unit
from typing import Optional
class Usecase:
def __init__(self, file, name="foot", conversion_offset=None):
def __init__(self, file: ifcopenshell.file, name: str = "foot", conversion_offset: Optional[float] = None):
"""Add a conversion based unit
If you're in one of those countries who don't use SI units, you're
@@ -41,7 +42,7 @@ class Usecase:
that this is just an example and you don't actually need to specify
that for fahrenheit as it's built into this API function. For
advanced users only.
:type conversion_offset: float
:type conversion_offset: float, optional
:return: The new IfcConversionBasedUnit or
IfcConversionBasedUnitWithOffset
:rtype: ifcopenshell.entity_instance.entity_instance
@@ -60,7 +61,7 @@ class Usecase:
self.file = file
self.settings = {"name": name, "conversion_offset": conversion_offset}
def execute(self):
def execute(self) -> ifcopenshell.entity_instance:
unit_type = ifcopenshell.util.unit.imperial_types.get(self.settings["name"], "USERDEFINED")
dimensions = ifcopenshell.util.unit.named_dimensions[unit_type]
exponents = self.file.createIfcDimensionalExponents(*dimensions)
@@ -17,10 +17,11 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.unit
from typing import Optional
class Usecase:
def __init__(self, file, unit_type="LENGTHUNIT", prefix=None):
def __init__(self, file: ifcopenshell.file, unit_type: str = "LENGTHUNIT", prefix: Optional[str] = None):
"""Add a new SI unit
The supported types are ABSORBEDDOSEUNIT, AMOUNTOFSUBSTANCEUNIT,
@@ -59,7 +60,7 @@ class Usecase:
self.file = file
self.settings = {"unit_type": unit_type, "prefix": prefix}
def execute(self):
def execute(self) -> ifcopenshell.entity_instance:
name = ifcopenshell.util.unit.si_type_names.get(self.settings["unit_type"], None)
return self.file.create_entity(
"IfcSIUnit", UnitType=self.settings["unit_type"], Name=name, Prefix=self.settings["prefix"]
@@ -18,10 +18,18 @@
import ifcopenshell
import ifcopenshell.util.unit
from typing import Optional
class Usecase:
def __init__(self, file, units=None, length=None, area=None, volume=None):
def __init__(
self,
file: ifcopenshell.file,
units: Optional[list[ifcopenshell.entity_instance]] = None,
length: Optional[dict] = None,
area: Optional[dict] = None,
volume: Optional[dict] = None,
):
"""Assign default project units
Whenever a unitised quantity is specified, such as a length, area,
@@ -67,7 +75,7 @@ class Usecase:
self.settings["area"] = area or {"is_metric": True, "raw": "METERS"}
self.settings["volume"] = volume or {"is_metric": True, "raw": "METERS"}
def execute(self):
def execute(self) -> ifcopenshell.entity_instance:
# We're going to refactor this to split unit creation and assignment
if self.settings["units"]:
units = self.settings["units"]
@@ -84,7 +92,7 @@ class Usecase:
self.assign_units(unit_assignment, units)
return unit_assignment
def get_unit_assignment(self):
def get_unit_assignment(self) -> ifcopenshell.entity_instance:
unit_assignment = self.file.by_type("IfcUnitAssignment")
if unit_assignment:
unit_assignment = unit_assignment[0]
@@ -97,13 +105,15 @@ class Usecase:
self.file.by_type("IfcContext")[0].UnitsInContext = unit_assignment
return unit_assignment
def assign_units(self, unit_assignment, new_units):
def assign_units(
self, unit_assignment: ifcopenshell.entity_instance, new_units: list[ifcopenshell.entity_instance]
) -> None:
units = set(unit_assignment.Units or [])
for unit in new_units:
units.add(unit)
unit_assignment.Units = list(units)
def create_metric_unit(self, unit_type, data):
def create_metric_unit(self, unit_type: str, data: dict) -> ifcopenshell.entity_instance:
type_prefix = ""
if unit_type == "area":
type_prefix = "SQUARE_"
@@ -116,7 +126,7 @@ class Usecase:
type_prefix + ifcopenshell.util.unit.get_unit_name(data["raw"]),
)
def create_imperial_unit(self, unit_type, data):
def create_imperial_unit(self, unit_type: str, data: dict) -> ifcopenshell.entity_instance:
if unit_type == "length":
dimensional_exponents = self.file.createIfcDimensionalExponents(1, 0, 0, 0, 0, 0, 0)
name_prefix = ""
@@ -15,10 +15,12 @@
#
# 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, units=None):
def __init__(self, file: ifcopenshell.file, units: Optional[list[ifcopenshell.entity_instance]] = None):
"""Unassigns units as default units for the project
:param units: A list of units to assign as project defaults.
@@ -23,7 +23,9 @@ import ifcopenshell.util.placement
class Usecase:
def __init__(self, file, opening=None, element=None):
def __init__(
self, file: ifcopenshell.file, opening: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance
):
"""Create an opening in an element
It is often necessary to cut out openings in elements like walls and
@@ -103,18 +105,18 @@ class Usecase:
self.file = file
self.settings = {"opening": opening, "element": element}
def execute(self):
def execute(self) -> ifcopenshell.entity_instance:
voids_elements = self.settings["opening"].VoidsElements
if voids_elements:
if voids_elements[0].RelatingBuildingElement == self.settings["element"]:
return
return voids_elements[0]
history = voids_elements[0].OwnerHistory
self.file.remove(voids_elements[0])
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
self.file.create_entity(
rel = self.file.create_entity(
"IfcRelVoidsElement",
**{
"GlobalId": ifcopenshell.guid.new(),
@@ -133,3 +135,5 @@ class Usecase:
matrix=ifcopenshell.util.placement.get_local_placement(self.settings["opening"].ObjectPlacement),
is_si=False,
)
return rel
@@ -17,10 +17,11 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, opening=None):
def __init__(self, file: ifcopenshell.entity_instance, opening: ifcopenshell.entity_instance):
"""Remove an opening
Fillings are retained as orphans. Voided elements remain. Openings
@@ -47,7 +48,7 @@ class Usecase:
self.file = file
self.settings = {"opening": opening}
def execute(self):
def execute(self) -> None:
for rel in self.settings["opening"].VoidsElements:
history = rel.OwnerHistory
self.file.remove(rel)
@@ -67,8 +67,9 @@ def set_unsupported_attribute(*args):
_method_dict = {}
def register_schema_attributes(schema):
def register_schema_attributes(schema: ifcopenshell_wrapper.schema_definition) -> None:
for decl in schema.declarations():
decl: ifcopenshell_wrapper.declaration
if hasattr(decl, "argument_types"):
fq_name = ".".join((schema.name(), decl.name()))
@@ -276,7 +277,15 @@ class entity_instance(object):
def __setattr__(self, key: str, value: Any) -> None:
index = self.wrapped_data.get_argument_index(key)
self[index] = value
try:
self[index] = value
except IndexError as e:
# get_argument_index returns 0xFFFFFFFF if attribute is not found
if index == 0xFFFFFFFF:
raise AttributeError(
"entity instance of type '%s' has no attribute '%s'" % (self.wrapped_data.is_a(True), key)
)
raise e
def __getitem__(self, key: int) -> Any:
if key < 0 or key >= len(self):
@@ -294,7 +303,15 @@ class entity_instance(object):
if value is None:
if method is not set_derived_attribute:
self.wrapped_data.setArgumentAsNull(idx)
try:
self.wrapped_data.setArgumentAsNull(idx)
except RuntimeError as e:
if e.args == ("Attribute not set",):
raise ValueError(
"attribute '%s' is not optional for entity instance of type '%s'"
% (self.wrapped_data.get_argument_name(idx), self.wrapped_data.is_a(True))
)
raise e
else:
self.method_list[idx](self.wrapped_data, idx, entity_instance.unwrap_value(value))
+7 -4
View File
@@ -28,7 +28,7 @@ import numbers
import zipfile
import functools
from pathlib import Path
from typing import List, Optional
from typing import Optional, Any
import ifcopenshell.util.element
import ifcopenshell.util.file
@@ -52,7 +52,7 @@ class Transaction:
self.batch_delete_ids = set()
self.batch_inverses = []
def serialise_entity_instance(self, element):
def serialise_entity_instance(self, element: ifcopenshell.entity_instance) -> dict[str, Any]:
info = element.get_info()
for key, value in info.items():
info[key] = self.serialise_value(element, value)
@@ -103,7 +103,7 @@ class Transaction:
}
)
def store_delete(self, element):
def store_delete(self, element: ifcopenshell.entity_instance) -> None:
inverses = {}
if self.is_batched:
if element.id() not in self.batch_delete_ids:
@@ -259,7 +259,7 @@ class file(object):
self.history_size = 64
self.history = []
self.future = []
self.transaction = None
self.transaction: Optional[Transaction] = None
import weakref
@@ -457,6 +457,9 @@ class file(object):
:type type: string
:param include_subtypes: Whether or not to return subtypes of the IFC class
:type include_subtypes: bool
:raises RuntimeError: If `type` is not found in IFC schema.
:returns: A list of ifcopenshell.entity_instance.entity_instance objects
:rtype: list[ifcopenshell.entity_instance.entity_instance]
"""
@@ -20,8 +20,10 @@ import json
from pathlib import Path
import copy
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
import ifcopenshell.util.attribute
import ifcopenshell.util.schema
from typing import Optional, Literal
try:
import glob
@@ -58,8 +60,8 @@ IFC4x3_SPEC_URL_TEMPLATE = "https://ifc43-docs.standards.buildingsmart.org/IFC/R
# child -> description
# note: in IFC4x3 there is no children[] for properties
SCHEMA_FILES = {
SUPPORTED_SCHEMA = Literal["IFC2X3", "IFC4", "IFC4X3"]
SCHEMA_FILES: dict[SUPPORTED_SCHEMA, dict] = {
"IFC2X3": {
"entities": BASE_MODULE_PATH / "schema/ifc2x3_entities.json",
"properties": BASE_MODULE_PATH / "schema/ifc2x3_properties.json",
@@ -81,7 +83,11 @@ SCHEMA_FILES = {
}
db = None
schema_by_name = {"IFC2X3": None, "IFC4": None, "IFC4X3": None}
schema_by_name: dict[SUPPORTED_SCHEMA, Optional[ifcopenshell_wrapper.schema_definition]] = {
"IFC2X3": None,
"IFC4": None,
"IFC4X3": None,
}
def get_db(version):
@@ -103,11 +109,12 @@ def get_db(version):
return db.get(version)
def get_schema_by_name(version: str):
def get_schema_by_name(version: str) -> ifcopenshell_wrapper.schema_definition:
global schema_by_name
version = ifcopenshell.util.schema.get_fallback_schema(version)
if not schema_by_name[version]:
schema_by_name[version] = ifcopenshell.ifcopenshell_wrapper.schema_by_name(version)
schema_name = "IFC4X3_ADD2" if version == "IFC4X3" else version
schema_by_name[version] = ifcopenshell_wrapper.schema_by_name(schema_name)
return schema_by_name[version]
@@ -512,6 +512,9 @@ def get_shape_aspects(element: ifcopenshell.entity_instance) -> list[ifcopenshel
if (representation := getattr(element, "Representation", ...)) != ...:
return representation.HasShapeAspects
if element.file.schema == "IFC2X3":
return []
# IfcTypeProduct
shape_aspects = []
for repersentation_map in element.RepresentationMaps:
@@ -17,10 +17,13 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import numpy as np
import numpy.typing as npt
import ifcopenshell
from typing import Literal, Iterable
MatrixType = np.ndarray[np.ndarray[float]]
MatrixType = npt.NDArray[np.float64]
"""`npt.NDArray[np.float64]`"""
def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> MatrixType:
@@ -36,7 +39,7 @@ def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> MatrixTyp
:param x: The +X vector / axis of the matrix
:type x: iterable[float]
:return: A 4x4 numpy matrix
:rtype: np.ndarray[np.ndarray[float]]
:rtype: MatrixType
"""
x = x / np.linalg.norm(x)
z = z / np.linalg.norm(z)
@@ -59,7 +62,7 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType:
:param placement: The IfcLocalPlacement enitity
:type placement: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy matrix
:rtype: np.ndarray[np.ndarray[float]]
:rtype: MatrixType
"""
ifc_class = placement.is_a()
if ifc_class in ("IfcAxis2Placement3D", "IfcAxis2PlacementLinear"):
@@ -69,10 +72,10 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType:
if coordinates := getattr(location, "Coordinates", None):
o = coordinates
else:
ifc_class = location.is_a("IfcPointByDistanceExpression")
ifc_class = location.is_a()
print(
f'WARNING. Placement location of type "{ifc_class}" '
f'is not yet supported and placement {placement} may be placed incorrectly.'
f"is not yet supported and placement {placement} may be placed incorrectly."
)
o = (0.0, 0.0, 0.0)
@@ -117,14 +120,14 @@ def get_local_placement(placement: ifcopenshell.entity_instance) -> MatrixType:
:param placement: The IfcLocalPlacement entity
:type placement: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy matrix
:rtype: np.ndarray[np.ndarray[float]]
:rtype: MatrixType
"""
if placement is None:
return np.eye(4)
if placement.PlacementRelTo is None:
if (rel_to := placement.PlacementRelTo) is None:
parent = np.eye(4)
else:
parent = get_local_placement(placement.PlacementRelTo)
parent = get_local_placement(rel_to)
return np.dot(parent, get_axis2placement(placement.RelativePlacement))
@@ -137,7 +140,7 @@ def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) ->
:param item: The IfcCartesianTransformationOperator entity
:type item: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy transformation matrix
:rtype: np.ndarray[np.ndarray[float]]
:rtype: MatrixType
"""
origin = np.array(inst.LocalOrigin.Coordinates)
axis1 = np.array((1.0, 0.0, 0.0))
@@ -183,7 +186,7 @@ def get_mappeditem_transformation(item: ifcopenshell.entity_instance) -> MatrixT
:param item: The IfcMappedItem entity
:type item: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy transformation matrix
:rtype: np.ndarray[np.ndarray[float]]
:rtype: MatrixType
"""
m4 = get_axis2placement(item.MappingSource.MappingOrigin)
# TODO 2d
@@ -219,7 +222,7 @@ def rotation(angle: float, axis: Literal["X", "Y", "Z"], is_degrees=True) -> Mat
radians. Defaults to true (i.e. degrees).
:type is_degrees: bool
:return: A 4x4 numpy rotation matrix
:rtype: np.ndarray[np.ndarray[float]]
:rtype: MatrixType
"""
theta = np.radians(angle) if is_degrees else angle
cos, sin = np.cos(theta), np.sin(theta)
@@ -20,7 +20,20 @@ import datetime
import ifcopenshell.util.date
from math import floor
from functools import lru_cache
from collections import namedtuple
from typing import Union, Literal, Optional, Iterator
DURATION_TYPE = Literal["ELAPSEDTIME", "WORKTIME", "NOTDEFINED"]
RECURRENCE_TYPE = Literal[
"BY_DAY_COUNT",
"BY_WEEKDAY_COUNT",
"DAILY",
"MONTHLY_BY_DAY_OF_MONTH",
"MONTHLY_BY_POSITION",
"WEEKLY",
"YEARLY_BY_DAY_OF_MONTH",
"YEARLY_BY_POSITION",
]
def derive_date(task, attribute_name, date=None, is_earliest=False, is_latest=False):
@@ -49,7 +62,7 @@ def derive_date(task, attribute_name, date=None, is_earliest=False, is_latest=Fa
return date
def derive_calendar(task):
def derive_calendar(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
calendar = get_calendar(task)
if calendar:
return calendar
@@ -57,7 +70,7 @@ def derive_calendar(task):
return derive_calendar(rel.RelatingObject)
def get_calendar(task):
def get_calendar(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
calendar = [
rel.RelatingControl
for rel in task.HasAssignments or []
@@ -68,7 +81,7 @@ def get_calendar(task):
return calendar[0]
def count_working_days(start, finish, calendar):
def count_working_days(start, finish, calendar: ifcopenshell.entity_instance) -> int:
result = 0
if start == finish:
return 0
@@ -88,7 +101,11 @@ def count_working_days(start, finish, calendar):
def get_start_or_finish_date(
start, duration, duration_type, calendar, date_type="FINISH"
start,
duration,
duration_type: DURATION_TYPE,
calendar: ifcopenshell.entity_instance,
date_type: Literal["START", "FINISH"] = "FINISH",
):
if not duration.days:
# Typically a milestone will have zero duration, so the start == finish
@@ -107,7 +124,7 @@ def get_start_or_finish_date(
return datetime.datetime.combine(result, datetime.time(17))
def offset_date(start, duration, duration_type, calendar):
def offset_date(start, duration, duration_type: DURATION_TYPE, calendar: ifcopenshell.entity_instance):
current_date = start
months = getattr(duration, "months", 0)
years = getattr(duration, "years", 0)
@@ -129,7 +146,7 @@ def offset_date(start, duration, duration_type, calendar):
return current_date
def get_soonest_working_day(start, duration_type, calendar):
def get_soonest_working_day(start, duration_type: DURATION_TYPE, calendar: ifcopenshell.entity_instance):
if duration_type == "ELAPSEDTIME" or not is_calendar_applicable(start, calendar):
return start
while not is_working_day(start, calendar):
@@ -139,7 +156,7 @@ def get_soonest_working_day(start, duration_type, calendar):
return start
def get_recent_working_day(start, duration_type, calendar):
def get_recent_working_day(start, duration_type: DURATION_TYPE, calendar: ifcopenshell.entity_instance):
if duration_type == "ELAPSEDTIME" or not is_calendar_applicable(start, calendar):
return start
while not is_working_day(start, calendar):
@@ -150,7 +167,7 @@ def get_recent_working_day(start, duration_type, calendar):
@lru_cache(maxsize=None)
def is_working_day(day, calendar):
def is_working_day(day, calendar: ifcopenshell.entity_instance) -> bool:
is_working_day = False
for work_time in calendar.WorkingTimes or []:
if is_work_time_applicable_to_day(work_time, day):
@@ -166,7 +183,7 @@ def is_working_day(day, calendar):
@lru_cache(maxsize=None)
def is_calendar_applicable(day, calendar):
def is_calendar_applicable(day, calendar: ifcopenshell.entity_instance) -> bool:
if not calendar or not calendar.WorkingTimes:
return False
is_applicable = False
@@ -177,18 +194,20 @@ def is_calendar_applicable(day, calendar):
return is_applicable
def is_day_in_work_time(day, work_time):
def is_day_in_work_time(day, work_time: ifcopenshell.entity_instance) -> bool:
is_day_in_work_time = True
if isinstance(day, datetime.datetime):
day = datetime.date(day.year, day.month, day.day)
if work_time[4]:
start = ifcopenshell.util.date.ifc2datetime(work_time[4])
# 4 IfcWorktime Start
if start := work_time[4]:
start = ifcopenshell.util.date.ifc2datetime(start)
if day > start:
is_day_in_work_time = True
else:
is_day_in_work_time = False
if work_time[5]:
finish = ifcopenshell.util.date.ifc2datetime(work_time[5])
# 5 IfcWorktime Finish
if finish := work_time[5]:
finish = ifcopenshell.util.date.ifc2datetime(finish)
if day < finish:
is_day_in_work_time = True
else:
@@ -196,7 +215,7 @@ def is_day_in_work_time(day, work_time):
return is_day_in_work_time
def is_work_time_applicable_to_day(work_time, day):
def is_work_time_applicable_to_day(work_time: ifcopenshell.entity_instance, day) -> bool:
if not is_day_in_work_time(day, work_time):
return False
if not work_time.RecurrencePattern:
@@ -205,36 +224,39 @@ def is_work_time_applicable_to_day(work_time, day):
if isinstance(day, datetime.datetime):
day = datetime.date(day.year, day.month, day.day)
recurrence = work_time.RecurrencePattern
if recurrence.RecurrenceType == "DAILY":
recurrence_type: RECURRENCE_TYPE = recurrence.RecurrenceType
if recurrence_type == "DAILY":
if not recurrence.Interval and not recurrence.Occurrences:
return True
# 4 IfcWorktime Start
if not work_time[4]:
return False
return False # TODO
elif recurrence.RecurrenceType == "WEEKLY":
elif recurrence_type == "WEEKLY":
if not recurrence.Interval and not recurrence.Occurrences:
return (day.weekday() + 1) in recurrence.WeekdayComponent
# 4 IfcWorktime Start
if not work_time[4]:
return False
return False # TODO
elif recurrence.RecurrenceType == "MONTHLY_BY_DAY_OF_MONTH":
elif recurrence_type == "MONTHLY_BY_DAY_OF_MONTH":
if not recurrence.Interval and not recurrence.Occurrences:
return day.day in recurrence.DayComponent
return False # TODO
elif recurrence.RecurrenceType == "MONTHLY_BY_POSITION":
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 False # TODO
elif recurrence.RecurrenceType == "YEARLY_BY_DAY_OF_MONTH":
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 False # TODO
elif recurrence.RecurrenceType == "YEARLY_BY_POSITION":
elif recurrence_type == "YEARLY_BY_POSITION":
if not recurrence.Interval and not recurrence.Occurrences:
return (
day.month in recurrence.MonthComponent
@@ -244,7 +266,7 @@ def is_work_time_applicable_to_day(work_time, day):
return False # TODO
def get_task_work_schedule(task):
def get_task_work_schedule(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
parent_task = get_parent_task(task)
if parent_task:
return get_task_work_schedule(parent_task) or get_task_work_schedule(task)
@@ -257,25 +279,23 @@ def get_task_work_schedule(task):
return None
def get_nested_tasks(task):
def get_nested_tasks(task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return [object for rel in task.IsNestedBy or [] for object in rel.RelatedObjects]
def get_parent_task(task):
return (
task.Nests[0].RelatingObject
if task.Nests and task.Nests[0].RelatingObject.is_a("IfcTask")
else None
)
def get_parent_task(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
nests = task.Nests
if nests and (obj := nests[0].RelatingObject).is_a("IfcTask"):
return obj
def get_all_nested_tasks(task):
def get_all_nested_tasks(task: ifcopenshell.entity_instance) -> Iterator[ifcopenshell.entity_instance]:
for nested_task in get_nested_tasks(task):
yield nested_task
yield from get_all_nested_tasks(nested_task)
def get_work_schedule_tasks(work_schedule):
def get_work_schedule_tasks(work_schedule: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
tasks = []
for root_task in get_root_tasks(work_schedule):
nested_tasks = get_all_nested_tasks(root_task)
@@ -283,7 +303,7 @@ def get_work_schedule_tasks(work_schedule):
return tasks
def get_root_tasks(work_schedule):
def get_root_tasks(work_schedule: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return [
obj
for rel in work_schedule.Controls
@@ -292,7 +312,7 @@ def get_root_tasks(work_schedule):
]
def get_root_tasks_ids(work_schedule):
def get_root_tasks_ids(work_schedule: ifcopenshell.entity_instance) -> list[int]:
return [
obj.id()
for rel in work_schedule.Controls
@@ -301,7 +321,7 @@ def get_root_tasks_ids(work_schedule):
]
def guess_date_range(work_schedule):
def guess_date_range(work_schedule: ifcopenshell.entity_instance):
earliest = None
latest = None
root_tasks = get_root_tasks(work_schedule)
@@ -323,7 +343,7 @@ def guess_date_range(work_schedule):
return earliest, latest
def get_direct_task_outputs(task):
def get_direct_task_outputs(task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return [
rel.RelatingProduct
for rel in task.HasAssignments
@@ -331,7 +351,7 @@ def get_direct_task_outputs(task):
]
def get_task_outputs(task, is_deep=False):
def get_task_outputs(task: ifcopenshell.entity_instance, is_deep=False):
if not is_deep:
return get_direct_task_outputs(task)
else:
@@ -342,7 +362,7 @@ def get_task_outputs(task, is_deep=False):
]
def get_task_inputs(task, is_deep=False):
def get_task_inputs(task: ifcopenshell.entity_instance, is_deep=False):
if not is_deep:
return [
object
@@ -365,7 +385,7 @@ def get_task_inputs(task, is_deep=False):
]
def get_task_resources(task, is_deep=False):
def get_task_resources(task: ifcopenshell.entity_instance, is_deep=False):
if not is_deep:
return [
object
@@ -388,15 +408,17 @@ def get_task_resources(task, is_deep=False):
]
def has_task_outputs(task):
def has_task_outputs(task: ifcopenshell.entity_instance) -> bool:
return len(get_task_outputs(task)) > 0
def has_task_inputs(task):
def has_task_inputs(task: ifcopenshell.entity_instance) -> bool:
return len(get_task_inputs(task)) > 0
def get_tasks_for_product(product, schedule=None):
def get_tasks_for_product(
product: ifcopenshell.entity_instance, schedule: Optional[ifcopenshell.entity_instance] = None
) -> tuple[list[ifcopenshell.entity_instance], list[ifcopenshell.entity_instance]]:
"""
Get all tasks assigned to or referenced by the given product.
@@ -438,7 +460,7 @@ def get_tasks_for_product(product, schedule=None):
return inputs, outputs
def get_sequence_assignment(task, sequence="successor"):
def get_sequence_assignment(task: ifcopenshell.entity_instance, sequence="successor"):
if sequence == "successor":
relationship_attr = "IsPredecessorTo"
elif sequence == "predecessor":
@@ -19,6 +19,7 @@
import shapely
import shapely.ops
import numpy as np
import numpy.typing as npt
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
@@ -28,6 +29,9 @@ tol = 1e-6
AXIS_LITERAL = Literal["X", "Y", "Z"]
VECTOR_3D = tuple[float, float, float]
MatrixType = npt.NDArray[np.float64]
"""`npt.NDArray[np.float64]`"""
def is_x(value: float, x: float, tolerance: Optional[float] = None) -> bool:
"""Checks whether a value is equivalent to X given a tolerance
@@ -113,19 +117,19 @@ def get_z(geometry) -> float:
return max(z_values) - min(z_values)
def get_shape_matrix(shape) -> np.ndarray:
def get_shape_matrix(shape) -> MatrixType:
"""Formats the transformation matrix of a shape as a 4x4 numpy array
:param shape: Shape output calculated by IfcOpenShell
:type shape: shape
:return: A 4x4 numpy array representing the transformation matrix
:rtype: np.array
:rtype: MatrixType
"""
m = shape.transformation.matrix.data
return np.array(([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]))
def get_bbox_centroid(geometry) -> tuple[float]:
def get_bbox_centroid(geometry) -> tuple[float, float, float]:
"""Calculates the bounding box centroid of the geometry
The centroid is in local coordinates relative to the object's placement.
@@ -133,11 +137,14 @@ def get_bbox_centroid(geometry) -> tuple[float]:
:param geometry: Geometry output calculated by IfcOpenShell
:type geometry: geometry
:return: A tuple representing the XYZ centroid
:rtype: tuple[float]
:rtype: tuple[float, float, float]
"""
x_values = [geometry.verts[i] for i in range(0, len(geometry.verts), 3)]
y_values = [geometry.verts[i + 1] for i in range(0, len(geometry.verts), 3)]
z_values = [geometry.verts[i + 2] for i in range(0, len(geometry.verts), 3)]
x_values: list[float]
y_values: list[float]
z_values: list[float]
minx = min(x_values)
maxx = max(x_values)
miny = min(y_values)
@@ -147,7 +154,7 @@ def get_bbox_centroid(geometry) -> tuple[float]:
return (minx + ((maxx - minx) / 2), miny + ((maxy - miny) / 2), minz + ((maxz - minz) / 2))
def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -> tuple[float]:
def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -> npt.NDArray[np.float64]:
"""Calculates the element's bounding box centroid
The centroid is in global coordinates. Note that if you have the shape, it
@@ -158,16 +165,16 @@ def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -
:param geometry: Geometry output calculated by IfcOpenShell
:type geometry: geometry
:return: A tuple representing the XYZ centroid
:rtype: tuple[float]
:rtype: npt.NDArray[np.float64]
"""
centroid = get_bbox_centroid(geometry)
if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"):
return centroid
return np.array(centroid)
mat = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
return (mat @ np.array([*centroid, 1.0]))[0:3]
def get_shape_bbox_centroid(shape, geometry) -> tuple[float]:
def get_shape_bbox_centroid(shape, geometry) -> npt.NDArray[np.float64]:
"""Calculates the shape's bounding box centroid
The centroid is in global coordinates. Note that if you do not have the
@@ -178,13 +185,13 @@ def get_shape_bbox_centroid(shape, geometry) -> tuple[float]:
:param geometry: Geometry output calculated by IfcOpenShell
:type geometry: geometry
:return: A tuple representing the XYZ centroid
:rtype: tuple[float]
:rtype: npt.NDArray[np.float64]
"""
centroid = get_bbox_centroid(geometry)
return (get_shape_matrix(shape) @ np.array([*centroid, 1.0]))[0:3]
def get_vertices(geometry) -> np.ndarray[np.ndarray[float]]:
def get_vertices(geometry) -> npt.NDArray[np.float64]:
"""Get all the vertices as a numpy array
Vertices are in local coordinates.
@@ -200,7 +207,7 @@ def get_vertices(geometry) -> np.ndarray[np.ndarray[float]]:
return np.array([np.array([verts[i], verts[i + 1], verts[i + 2]]) for i in range(0, len(verts), 3)])
def get_edges(geometry) -> np.ndarray[np.ndarray[int]]:
def get_edges(geometry) -> npt.NDArray[np.int32]:
"""Get all the edges as a numpy array
Results are a nested numpy array e.g. [[e1v1, e1v2], [e2v1, e2v2], ...]
@@ -215,10 +222,10 @@ def get_edges(geometry) -> np.ndarray[np.ndarray[int]]:
:rtype: np.array[np.array[int]]
"""
edges = geometry.edges
return [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
return np.array([[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)])
def get_faces(geometry) -> np.ndarray[np.ndarray[int]]:
def get_faces(geometry) -> npt.NDArray[np.int32]:
"""Get all the faces as a numpy array
Faces are always triangulated. If the shape is a BRep and you want to get
@@ -232,10 +239,10 @@ def get_faces(geometry) -> np.ndarray[np.ndarray[int]]:
:rtype: np.array[np.array[int]]
"""
faces = geometry.faces
return [[faces[i], faces[i + 1], faces[i + 2]] for i in range(0, len(faces), 3)]
return np.array([[faces[i], faces[i + 1], faces[i + 2]] for i in range(0, len(faces), 3)])
def get_shape_vertices(shape, geometry) -> np.ndarray[np.ndarray[float]]:
def get_shape_vertices(shape, geometry) -> npt.NDArray[np.float64]:
"""Get the shape's vertices as a numpy array
Vertices are in global coordinates. If you do not have the shape, you can
@@ -255,7 +262,7 @@ def get_shape_vertices(shape, geometry) -> np.ndarray[np.ndarray[float]]:
return np.delete((mat @ np.hstack((verts, np.ones((len(verts), 1)))).T).T, -1, axis=1)
def get_element_vertices(element: ifcopenshell.entity_instance, geometry) -> np.ndarray[np.ndarray[float]]:
def get_element_vertices(element: ifcopenshell.entity_instance, geometry) -> npt.NDArray[np.float64]:
"""Get the element's vertices as a numpy array
Vertices are in global coordinates. Note that if you have the shape, it is
@@ -365,7 +372,7 @@ def get_element_top_elevation(element: ifcopenshell.entity_instance, geometry) -
return max([v[2] for v in get_element_vertices(element, geometry)])
def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[np.ndarray[float]]:
def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
"""Gets the bounding box of vertices
:param vertices: An iterable of vertices
@@ -388,7 +395,7 @@ def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[np.ndarray[float]]:
return (np.array([minx, miny, minz]), np.array([maxx, maxy, maxz]))
def get_area_vf(vertices: np.ndarray[VECTOR_3D], faces: np.ndarray[Iterable[int]]) -> float:
def get_area_vf(vertices: npt.NDArray[np.float64], faces: npt.NDArray[np.int32]) -> float:
"""Calculates the surface area given a list of vertices and triangulated faces
:param vertices: A list of 3D vertices, such as returned from get_vertices.
@@ -17,9 +17,14 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import numpy as np
import numpy.typing as npt
import collections
import collections.abc
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.unit
from math import cos, sin, pi, tan, radians, degrees, atan, sqrt, ceil
from typing import List, Tuple, Type, Union
from itertools import chain
@@ -339,6 +344,16 @@ class ShapeBuilder:
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcArbitraryClosedProfileDef.htm#8.15.3.1.4-Formal-propositions"
)
kwargs = {
"ProfileName": name,
"ProfileType": profile_type,
"OuterCurve": outer_curve,
}
if self.file.schema == "IFC2X3":
kwargs["Position"] = self.file.create_entity(
"IfcAxis2Placement2D", self.file.create_entity("IfcCartesianPoint", [0.0, 0.0])
)
if inner_curves:
if not isinstance(inner_curves, collections.abc.Iterable):
inner_curves = [inner_curves]
@@ -349,13 +364,9 @@ class ShapeBuilder:
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcArbitraryClosedProfileDef.htm#8.15.3.1.4-Formal-propositions"
)
profile = self.file.createIfcArbitraryProfileDefWithVoids(
ProfileName=name, ProfileType=profile_type, OuterCurve=outer_curve, InnerCurves=inner_curves
)
profile = self.file.create_entity("IfcArbitraryProfileDefWithVoids", InnerCurves=inner_curves, **kwargs)
else:
profile = self.file.createIfcArbitraryClosedProfileDef(
ProfileName=name, ProfileType=profile_type, OuterCurve=outer_curve
)
profile = self.file.create_entity("IfcArbitraryClosedProfileDef", **kwargs)
return profile
def translate(self, curve_or_item, translation: Vector, create_copy=False):
@@ -529,13 +540,13 @@ class ShapeBuilder:
def create_axis2_placement_3d_from_matrix(
self,
matrix: Union[np.ndarray, None] = None,
matrix: Union[npt.NDArray[np.float64], None] = None,
) -> ifcopenshell.entity_instance:
"""
Create IfcAxis2Placement3D from numpy matrix.
:param matrix: 4x4 transformation matrix, defaults to `np.eye(4)`
:type matrix: np.array[np.array[float]], optional
:type matrix: npt.NDArray[np.float64], optional
:return: IfcAxis2Placement3D
:rtype: ifcopenshell.entity_instance
"""
@@ -21,6 +21,7 @@ from math import pi
from typing import Iterable, Any, Union, Literal, Optional
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
import ifcopenshell.api
prefixes = {
@@ -353,13 +354,32 @@ def get_prefix_multiplier(text):
return 1
def get_unit_name(text):
def get_unit_name(text: str) -> Union[str, None]:
"""Get unit name from str, if unit is in SI."""
text = text.upper().replace("METER", "METRE")
for name in unit_names:
if name.replace("_", " ") in text:
return name
def get_unit_name_universal(text: str) -> Union[str, None]:
"""Get unit name from str, supports both SI and imperial system.
Can be used to provide units for `convert()`"""
text = text.upper().replace("METER", "METRE")
for name in unit_names:
if name.replace("_", " ") in text:
return name
for name in imperial_types:
if name.upper() in text:
return name
def get_full_unit_name(unit: ifcopenshell.entity_instance) -> str:
prefix = getattr(unit, "Prefix", None) or ""
return prefix + unit.Name.upper()
def get_si_dimensions(name):
return si_dimensions.get(name, si_dimensions["OTHERWISE"])
@@ -368,13 +388,13 @@ def get_named_dimensions(name):
return named_dimensions.get(name, (0, 0, 0, 0, 0, 0, 0))
def get_unit_assignment(ifc_file):
def get_unit_assignment(ifc_file: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]:
unit_assignments = ifc_file.by_type("IfcUnitAssignment")
if unit_assignments:
return unit_assignments[0]
def get_project_unit(ifc_file, unit_type):
def get_project_unit(ifc_file: ifcopenshell.file, unit_type: str) -> Union[ifcopenshell.entity_instance, None]:
"""Get the default project unit of a particular unit type
:param ifc_file: The IFC file.
@@ -384,7 +404,7 @@ def get_project_unit(ifc_file, unit_type):
:type unit_type: str
:return: The IFC unit entity, or nothing if there is no default project unit
defined.
:rtype: ifcopenshell.entity_instance,None
:rtype: Union[ifcopenshell.entity_instance, None]
"""
unit_assignment = get_unit_assignment(ifc_file)
if unit_assignment:
@@ -393,7 +413,9 @@ def get_project_unit(ifc_file, unit_type):
return unit
def get_property_unit(prop, ifc_file):
def get_property_unit(
prop: ifcopenshell.entity_instance, ifc_file: ifcopenshell.file
) -> Union[ifcopenshell.entity_instance, None]:
unit = getattr(prop, "Unit", None)
if unit:
return unit
@@ -446,14 +468,14 @@ def get_property_unit(prop, ifc_file):
return units[0]
def get_unit_measure_class(unit_type):
def get_unit_measure_class(unit_type: str) -> str:
if unit_type == "USERDEFINED":
# See https://github.com/buildingSMART/IFC4.3.x-development/issues/71
return "IfcNumericMeasure"
return "Ifc" + unit_type[0:-4].lower().capitalize() + "Measure"
def get_measure_unit_type(measure_class):
def get_measure_unit_type(measure_class: str) -> str:
if measure_class == "IfcNumericMeasure":
# See https://github.com/buildingSMART/IFC4.3.x-development/issues/71
return "USERDEFINED"
@@ -462,7 +484,7 @@ def get_measure_unit_type(measure_class):
return measure_class.upper() + "UNIT"
def get_symbol_measure_class(symbol):
def get_symbol_measure_class(symbol: Optional[str] = None) -> str:
# Dumb, but everybody gets it, unlike regex golf
if not symbol:
return "IfcNumericMeasure"
@@ -480,7 +502,7 @@ def get_symbol_measure_class(symbol):
return "IfcNumericMeasure"
def get_symbol_quantity_class(symbol):
def get_symbol_quantity_class(symbol: Optional[str] = None) -> str:
# Dumb, but everybody gets it, unlike regex golf
if not symbol:
return "IfcQuantityCount"
@@ -498,7 +520,7 @@ def get_symbol_quantity_class(symbol):
return "IfcQuantityCount"
def get_unit_symbol(unit):
def get_unit_symbol(unit: ifcopenshell.entity_instance) -> str:
symbol = ""
if unit.is_a("IfcSIUnit"):
symbol += prefix_symbols.get(unit.Prefix, "")
@@ -508,7 +530,7 @@ def get_unit_symbol(unit):
return symbol
def convert_unit(value, from_unit, to_unit):
def convert_unit(value: float, from_unit: ifcopenshell.entity_instance, to_unit: ifcopenshell.entity_instance) -> float:
"""Convert from one unit to another unit
:param value: The numeric value you want to convert
@@ -668,9 +690,9 @@ def format_length(
def is_attr_type(
content_type: Union[ifcopenshell.ifcopenshell_wrapper.named_type, ifcopenshell.ifcopenshell_wrapper.type_declaration],
content_type: ifcopenshell_wrapper.parameter_type,
ifc_unit_type_name: str,
) -> Union[ifcopenshell.ifcopenshell_wrapper.type_declaration, None]:
) -> Union[ifcopenshell_wrapper.type_declaration, None]:
cur_decl = content_type
while hasattr(cur_decl, "declared_type") is True:
cur_decl = cur_decl.declared_type()
@@ -679,9 +701,15 @@ def is_attr_type(
if cur_decl.name() == ifc_unit_type_name:
return cur_decl
if isinstance(cur_decl, ifcopenshell.ifcopenshell_wrapper.aggregation_type):
res = cur_decl.type_of_element()
cur_decl = res.declared_type()
if isinstance(cur_decl, ifcopenshell_wrapper.aggregation_type):
# support aggregate of aggregates, as in IfcCartesianPointList3D.CoordList
def get_declared_type_from_aggregate(cur_decl):
cur_decl = cur_decl.type_of_element()
if not isinstance(cur_decl, ifcopenshell_wrapper.aggregation_type):
return cur_decl.declared_type()
return get_declared_type_from_aggregate(cur_decl)
cur_decl = get_declared_type_from_aggregate(cur_decl)
if hasattr(cur_decl, "name") and cur_decl.name() == ifc_unit_type_name:
return cur_decl
while hasattr(cur_decl, "declared_type") is True:
@@ -696,13 +724,13 @@ def is_attr_type(
def iter_element_and_attributes_per_type(
ifc_file: ifcopenshell.file, attr_type_name: str
) -> Iterable[tuple[ifcopenshell.entity_instance, ifcopenshell.ifcopenshell_wrapper.attribute, Any, str]]:
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema)
) -> Iterable[tuple[ifcopenshell.entity_instance, ifcopenshell_wrapper.attribute, Any]]:
schema: ifcopenshell_wrapper.schema_definition = ifcopenshell_wrapper.schema_by_name(ifc_file.schema)
for element in ifc_file:
entity = schema.declaration_by_name(element.is_a())
attrs = entity.all_attributes()
for i, (attr, val, is_derived) in enumerate(zip(attrs, list(element), entity.derived())):
for attr, val, is_derived in zip(attrs, list(element), entity.derived()):
if is_derived:
continue
@@ -718,26 +746,38 @@ def iter_element_and_attributes_per_type(
yield element, attr, val
def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str) -> ifcopenshell.file:
def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = "METER") -> ifcopenshell.file:
"""Converts all units in an IFC file to the specified target units. Returns a new file."""
prefix = "MILLI" if target_units == "MILLIMETERS" else None
prefix = get_prefix(target_units)
si_unit = get_unit_name(target_units)
# Copy all elements from the original file to the patched file
file_patched = ifcopenshell.file.from_string(ifc_file.wrapped_data.to_string())
unit_assignment = ifcopenshell.util.unit.get_unit_assignment(file_patched)
unit_assignment = get_unit_assignment(file_patched)
old_length = [u for u in unit_assignment.Units if getattr(u, "UnitType", None) == "LENGTHUNIT"][0]
new_length = ifcopenshell.api.run("unit.add_si_unit", file_patched, unit_type="LENGTHUNIT", prefix=prefix)
old_length = next(u for u in unit_assignment.Units if getattr(u, "UnitType", None) == "LENGTHUNIT")
if si_unit:
new_length = ifcopenshell.api.run("unit.add_si_unit", file_patched, unit_type="LENGTHUNIT", prefix=prefix)
else:
target_units = target_units.lower()
if imperial_types.get(target_units) != "LENGTHUNIT":
raise Exception(
f'Couldn\'t identify target units "{target_units}". '
'The method supports singular unit names like "CENTIMETER", "METER", "FOOT", etc.'
)
new_length = ifcopenshell.api.run("unit.add_conversion_based_unit", file_patched, name=target_units)
# support tuple of tuples, as in IfcCartesianPointList3D.CoordList
def convert_value(value):
if not isinstance(value, tuple):
return convert_unit(value, old_length, new_length)
return tuple(convert_value(v) for v in value)
# Traverse all elements and their nested attributes in the file and convert them
for element, attr, val in iter_element_and_attributes_per_type(file_patched, "IfcLengthMeasure"):
if isinstance(val, tuple):
new_value = [ifcopenshell.util.unit.convert_unit(v, old_length, new_length) for v in val]
setattr(element, attr.name(), tuple(new_value))
else:
new_value = ifcopenshell.util.unit.convert_unit(val, old_length, new_length)
setattr(element, attr.name(), new_value)
new_value = convert_value(val)
setattr(element, attr.name(), new_value)
file_patched.remove(old_length)
unit_assignment.Units = tuple([new_length, *unit_assignment.Units])
@@ -0,0 +1,79 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
FILE_NAME('mill.ifc','2024-04-23T16:52:20+05:00',(),(),'IfcOpenShell v0.7.0-f7c03db75','BlenderBIM 0.0.240422-998f9c6','Nobody');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('3Q4HLVId9C$OJq2NqKnvVO',$,'My Project',$,$,$,$,(#14,#26),#9);
#2=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#3=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#4=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#5=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#6=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#7=IFCMEASUREWITHUNIT(IFCREAL(0.0174532925199433),#6);
#8=IFCCONVERSIONBASEDUNIT(#5,.PLANEANGLEUNIT.,'degree',#7);
#9=IFCUNITASSIGNMENT((#4,#2,#8,#3));
#10=IFCCARTESIANPOINT((0.,0.,0.));
#11=IFCDIRECTION((0.,0.,1.));
#12=IFCDIRECTION((1.,0.,0.));
#13=IFCAXIS2PLACEMENT3D(#10,#11,#12);
#14=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#13,$);
#15=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$);
#16=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Model',*,*,*,*,#14,$,.GRAPH_VIEW.,$);
#17=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Box','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$);
#18=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.SECTION_VIEW.,$);
#19=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.ELEVATION_VIEW.,$);
#20=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$);
#21=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.PLAN_VIEW.,$);
#22=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Profile','Model',*,*,*,*,#14,$,.ELEVATION_VIEW.,$);
#23=IFCCARTESIANPOINT((0.,0.));
#24=IFCDIRECTION((1.,0.));
#25=IFCAXIS2PLACEMENT2D(#23,#24);
#26=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Plan',2,1.E-05,#25,$);
#27=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Plan',*,*,*,*,#26,$,.GRAPH_VIEW.,$);
#28=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Plan',*,*,*,*,#26,$,.PLAN_VIEW.,$);
#29=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#26,$,.PLAN_VIEW.,$);
#30=IFCSITE('1T8u08wvPCnhfVRFeG_Je7',$,'My Site',$,$,#53,$,$,$,$,$,$,$,$);
#36=IFCBUILDING('0AGAIai5z9HOTud5I6CjMh',$,'My Building',$,$,#59,$,$,$,$,$,$);
#42=IFCBUILDINGSTOREY('2lMUoFnMv5KOSnzZ9_MPZv',$,'My Storey',$,$,#65,$,$,$,$);
#48=IFCRELAGGREGATES('0cjacslobAjw8I_uTLtTuK',$,$,$,#1,(#30));
#49=IFCCARTESIANPOINT((0.,0.,0.));
#50=IFCDIRECTION((0.,0.,1.));
#51=IFCDIRECTION((1.,0.,0.));
#52=IFCAXIS2PLACEMENT3D(#49,#50,#51);
#53=IFCLOCALPLACEMENT($,#52);
#54=IFCRELAGGREGATES('126VmVXPD0pAP2uJz6QYat',$,$,$,#30,(#36));
#55=IFCCARTESIANPOINT((0.,0.,0.));
#56=IFCDIRECTION((0.,0.,1.));
#57=IFCDIRECTION((1.,0.,0.));
#58=IFCAXIS2PLACEMENT3D(#55,#56,#57);
#59=IFCLOCALPLACEMENT(#53,#58);
#60=IFCRELAGGREGATES('1KLZsOP9zAivCjkiTn31bt',$,$,$,#36,(#42));
#61=IFCCARTESIANPOINT((0.,0.,0.));
#62=IFCDIRECTION((0.,0.,1.));
#63=IFCDIRECTION((1.,0.,0.));
#64=IFCAXIS2PLACEMENT3D(#61,#62,#63);
#65=IFCLOCALPLACEMENT(#59,#64);
#66=IFCACTUATOR('3RT$GBDjj6VhZQ2OYV3P13',$,'Cube',$,$,#95,#84,$,.ELECTRICACTUATOR.);
#72=IFCINDEXEDPOLYGONALFACE((1,2,4,3));
#73=IFCINDEXEDPOLYGONALFACE((3,4,8,7));
#74=IFCINDEXEDPOLYGONALFACE((7,8,6,5));
#75=IFCINDEXEDPOLYGONALFACE((5,6,2,1));
#76=IFCINDEXEDPOLYGONALFACE((3,7,5,1));
#77=IFCINDEXEDPOLYGONALFACE((8,4,2,6));
#78=IFCCARTESIANPOINTLIST3D(((-999.999938964844,-999.999938964844,-999.999938964844),(-999.999938964844,-999.999938964844,999.999938964844),(-999.999938964844,999.999938964844,-999.999938964844),(-999.999938964844,999.999938964844,999.999938964844),(999.999938964844,-999.999938964844,-999.999938964844),(999.999938964844,-999.999938964844,999.999938964844),(999.999938964844,999.999938964844,-999.999938964844),(999.999938964844,999.999938964844,999.999938964844)));
#79=IFCPOLYGONALFACESET(#78,.T.,(#72,#73,#74,#75,#76,#77),$);
#80=IFCSHAPEREPRESENTATION(#15,'Body','Tessellation',(#79));
#81=IFCCARTESIANPOINT((-1000.,-1000.,-1000.));
#82=IFCBOUNDINGBOX(#81,2000.,2000.,2000.);
#83=IFCSHAPEREPRESENTATION(#17,'Box','BoundingBox',(#82));
#84=IFCPRODUCTDEFINITIONSHAPE($,$,(#83,#80));
#85=IFCRELCONTAINEDINSPATIALSTRUCTURE('107zrsI95BsfGJXI83megf',$,$,$,(#66),#42);
#91=IFCCARTESIANPOINT((0.,5000.,0.));
#92=IFCDIRECTION((0.,0.,1.));
#93=IFCDIRECTION((1.,0.,0.));
#94=IFCAXIS2PLACEMENT3D(#91,#92,#93);
#95=IFCLOCALPLACEMENT(#65,#94);
ENDSEC;
END-ISO-10303-21;
@@ -16,34 +16,52 @@
# 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 pathlib
import pytest
import ifcopenshell.util.unit
import numpy as np
UNITS_FIXTURE_DIR = pathlib.Path(__file__).parent.parent / "fixtures" / "units"
@pytest.mark.parametrize("ifc_file", UNITS_FIXTURE_DIR.glob("*.ifc"))
def test_file_units_length_convert(ifc_file):
def test_file_units_length_convert(ifc_file: str):
f = ifcopenshell.open(ifc_file)
project_unit = ifcopenshell.util.unit.get_project_unit(f, "LENGTHUNIT")
if project_unit.Prefix == "MILLI":
target_units = "METERS"
scale = 0.001
else:
target_units = "MILLIMETERS"
scale = 1000
new_f = ifcopenshell.util.unit.convert_file_length_units(f, target_units)
def get_project_unit(f: ifcopenshell.file) -> str:
unit = ifcopenshell.util.unit.get_project_unit(f, "LENGTHUNIT")
return ifcopenshell.util.unit.get_full_unit_name(unit)
base_project_unit = get_project_unit(f)
target_units = ["MILLIMETRE", "METRE", "CENTIMETRE", "INCH", "FOOT"]
def convert_file_and_test(f: ifcopenshell.file, project_unit: str, target_unit: str) -> ifcopenshell.file:
scale = ifcopenshell.util.unit.convert(
value=1,
from_prefix=ifcopenshell.util.unit.get_prefix(project_unit),
from_unit=ifcopenshell.util.unit.get_unit_name_universal(project_unit),
to_prefix=ifcopenshell.util.unit.get_prefix(target_unit),
to_unit=ifcopenshell.util.unit.get_unit_name_universal(target_unit),
)
new_f = ifcopenshell.util.unit.convert_file_length_units(f, target_unit)
for element, attr, val in ifcopenshell.util.unit.iter_element_and_attributes_per_type(
new_f, "IfcLengthMeasure"
):
elem_id = element.id()
original_element = f.by_id(elem_id)
original_val = getattr(original_element, attr.name())
def convert_value(value):
if not isinstance(value, tuple):
return value * scale
return tuple(convert_value(v) for v in value)
for element, attr, val in ifcopenshell.util.unit.iter_element_and_attributes_per_type(new_f, "IfcLengthMeasure"):
elem_id = element.id()
original_element = f.by_id(elem_id)
original_val = getattr(original_element, attr.name())
if isinstance(original_val, tuple):
# assert element is equal to original element times scale
assert val == tuple([v * scale for v in original_val])
else:
# assert element is equal to original element times scale
assert val == original_val * scale
assert np.allclose([val], [convert_value(original_val)])
assert get_project_unit(new_f) == target_unit
return new_f
for target_unit in target_units:
new_f = convert_file_and_test(f, base_project_unit, target_unit)
convert_file_and_test(new_f, target_unit, base_project_unit)
@@ -26,13 +26,19 @@ from logging import Logger
class Patcher:
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, unit: str = "METERS"):
def __init__(
self,
src: str,
file: ifcopenshell.file,
logger: Logger,
unit: str = "METER",
):
"""Converts the length unit of a model to the specified unit
Allowed metric units include METERS, MILLIMETERS, CENTIMETERS, etc.
Allowed imperial units include INCHES, FEET, MILES.
Allowed metric units include METER, MILLIMETER, CENTIMETER, etc.
Allowed imperial units include INCH, FOOT, MILE.
:param unit: The name of the desired unit, defaults to "METERS"
:param unit: The name of the desired unit, defaults to "METER"
:type unit: str
Example:
@@ -40,10 +46,10 @@ class Patcher:
.. code:: python
# Convert to millimeters
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["MILLIMETERS"]})
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["MILLIMETER"]})
# Convert to feet
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["FEET"]})
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["FOOT"]})
"""
self.src = src
self.file = file
@@ -18,6 +18,7 @@
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.unit
from typing import Union
from logging import Logger
@@ -30,6 +31,9 @@ class Patcher:
further processing will be done. This means that you may end up with
duplicate spatial hierarchies (i.e. 2 sites, 2 buildings, etc).
Will automatically convert length units in the second model to the main
model's unit before merging.
:param filepath: The filepath of the second IFC model to merge into the
first. The first model is already specified as the input to
IfcPatch.
@@ -48,6 +52,10 @@ class Patcher:
def patch(self):
source = ifcopenshell.open(self.filepath)
# make sure models units will match
if (main_unit := self.get_unit_name(self.file)) != self.get_unit_name(source):
source = ifcopenshell.util.unit.convert_file_length_units(source, main_unit)
self.existing_contexts: list[ifcopenshell.entity_instance] = self.file.by_type(
"IfcGeometricRepresentationContext"
)
@@ -69,6 +77,10 @@ class Patcher:
self.reuse_existing_contexts()
def get_unit_name(self, ifc_file: ifcopenshell.file) -> str:
length_unit = ifcopenshell.util.unit.get_project_unit(ifc_file, "LENGTHUNIT")
return ifcopenshell.util.unit.get_full_unit_name(length_unit)
def reuse_existing_contexts(self):
to_delete = set()
@@ -0,0 +1,39 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import test.bootstrap
import ifcopenshell.api
import ifcopenshell.util.unit
import ifcpatch
class TestConvertLengthUnit(test.bootstrap.IFC4):
# NOTE: conversion itself is covered by `ifcopenshell.util.unit.convert_file_length_units` tests
def test_run(self):
project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI")
ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
output = ifcpatch.execute(
{"input": "input.ifc", "file": self.file, "recipe": "ConvertLengthUnit", "arguments": ["METER"]}
)
unit = ifcopenshell.util.unit.get_project_unit(output, "LENGTHUNIT")
assert ifcopenshell.util.unit.get_full_unit_name(unit) == "METRE"
class TestConvertLengthUnitIFC2X3(test.bootstrap.IFC2X3, TestConvertLengthUnit):
pass
+66
View File
@@ -0,0 +1,66 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcpatch
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.placement
import test.bootstrap
import tempfile
import numpy as np
from pathlib import Path
from typing import Optional
class TestMergeProject(test.bootstrap.IFC4):
def setup_project(self, ifc_file: Optional[ifcopenshell.file] = None):
prefix = None if ifc_file else "MILLI"
if ifc_file is None:
ifc_file = ifcopenshell.file(schema=self.file.schema)
project = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcProject")
unit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", prefix=prefix)
ifcopenshell.api.run("unit.assign_unit", ifc_file, units=[unit])
matrix = np.eye(4)
matrix[:, 3] = (1, 2, 3, 1)
wall = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcWall")
ifcopenshell.api.run("geometry.edit_object_placement", ifc_file, product=wall, matrix=matrix, is_si=True)
return ifc_file
def test_run(self):
self.file = self.setup_project(self.file)
second_file = self.setup_project()
temp_path = Path(tempfile.gettempdir()) / "second.ifc"
second_file.write(temp_path)
output = ifcpatch.execute({"file": self.file, "recipe": "MergeProject", "arguments": [str(temp_path)]})
assert len(output.by_type("IfcWall")) == 2
wall1, wall2 = output.by_type("IfcWall")
# test that units are converted
placement1 = ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement)
placement2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement)
to_tuple = lambda arr: tuple(map(tuple, arr))
matrix = np.eye(4)
matrix[:, 3] = (1, 2, 3, 1)
assert to_tuple(placement1) == to_tuple(placement2) == to_tuple(matrix)
class TestMergeProjectIFC2X3(test.bootstrap.IFC2X3, TestMergeProject):
pass
+7
View File
@@ -72,9 +72,16 @@
template <>
std::string cast_pyobject(PyObject* element) {
#if SWIG_VERSION >= 0x040200
PyObject *pbytes = NULL;
const char* str_data = SWIG_PyUnicode_AsUTF8AndSize(element, NULL, &pbytes);
std::string str = str_data;
Py_XDECREF(pbytes);
#else
char* str_data = SWIG_Python_str_AsChar(element);
std::string str = str_data;
SWIG_Python_str_DelForPy3(str_data);
#endif
return str;
}