Compare commits

...

19 Commits

Author SHA1 Message Date
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
36 changed files with 605 additions and 174 deletions
+24 -22
View File
@@ -199,7 +199,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
@@ -725,7 +725,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):
@@ -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,27 +1629,27 @@ 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])
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")]
]
@@ -1810,7 +1812,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:
@@ -1959,14 +1961,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 +1988,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 +2003,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)
@@ -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="")
+6 -1
View File
@@ -94,7 +94,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):
+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,10 @@ 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:
return ifcopenshell.util.element.copy_deep(tool.Ifc.get(), 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
+5
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,10 @@ 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:
return ifcopenshell.util.element.copy_deep(tool.Ifc.get(), 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"
@@ -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)
@@ -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.
+3 -3
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:
@@ -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]
@@ -121,10 +121,10 @@ def get_local_placement(placement: ifcopenshell.entity_instance) -> 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))
@@ -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":
@@ -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;
}