Compare commits

...

31 Commits

Author SHA1 Message Date
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
51 changed files with 868 additions and 302 deletions
+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)
@@ -20,6 +20,7 @@ import bpy
import bmesh
import logging
import shapely
import shapely.ops
import mathutils
import numpy as np
import multiprocessing
@@ -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(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}
@@ -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")
@@ -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="")
+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, Identifiaction=identification)
ifc.run("document.edit_reference", reference=reference, attributes=attributes)
def load_schedules(drawing):
+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
@@ -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
+32 -12
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
+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
@@ -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
+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) {
@@ -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,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.
@@ -276,7 +276,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):
+4 -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
@@ -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]
@@ -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;
}