diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 213dd7a6fc..0907cf9739 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -466,6 +466,10 @@ ElSE() else() add_definitions(-Wno-maybe-uninitialized) endif() + if (CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 9.0 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 9.0)) + # OpenCascade spews a lot of deprecated-copy warnings + add_definitions(-Wno-deprecated-copy) + endif() # -fPIC is not relevant on Windows and creates pointless warnings if (UNIX) add_definitions(-fPIC) diff --git a/nix/build-all.py b/nix/build-all.py index ebae9742a8..dd9e63c61d 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -336,7 +336,7 @@ def git_clone_or_pull_repository(clone_url, target_dir, revision=None): `None`.""" if not os.path.exists(target_dir): logger.info("cloning '%s' into '%s'" % (clone_url, target_dir)) - run([git, "clone", clone_url, target_dir]) + run([git, "clone", "--recursive", clone_url, target_dir]) else: logger.info("directory '%s' already cloned. Pulling latest changes." % (target_dir,)) diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 1548f6b72d..4ee574bbe5 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -52,10 +52,10 @@ endif # Provides IfcOpenShell Python functionality ifeq ($(PYVERSION), py37) - cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-37-v0.6.0-517b819-$(PLATFORM)64.zip + cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-37-v0.6.0-ff7219b-$(PLATFORM)64.zip endif ifeq ($(PYVERSION), py39) - cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-39-v0.6.0-517b819-$(PLATFORM)64.zip + cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-39-v0.6.0-ff7219b-$(PLATFORM)64.zip endif cd dist/working && unzip ifcblender* cp -r dist/working/io_import_scene_ifc/ifcopenshell dist/blenderbim/libs/site/packages/ @@ -142,7 +142,7 @@ endif cd dist/working && unzip v0.6.0.zip # IfcOpenBot sometimes lags behind, so we hotfix the Python utilities cp -r dist/working/IfcOpenShell-0.6.0/src/ifcopenshell-python/ifcopenshell/util/* dist/blenderbim/libs/site/packages/ifcopenshell/util/ - cp -r dist/working/IfcOpenShell-0.6.0/src/ifcopenshell-python/ifcopenshell/api dist/blenderbim/libs/site/packages/ifcopenshell/api + cp -r dist/working/IfcOpenShell-0.6.0/src/ifcopenshell-python/ifcopenshell/api/* dist/blenderbim/libs/site/packages/ifcopenshell/api/ # Provides bcf functionality cp -r dist/working/IfcOpenShell-0.6.0/src/bcf/bcf dist/blenderbim/libs/site/packages/ # Provides IFCClash functionality diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 1678d8999d..121342189f 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -25,6 +25,7 @@ if bpy is not None: "geometry": None, "cobie": None, "sequence": None, + "cost": None, "group": None, "structural": None, "material": None, diff --git a/src/blenderbim/blenderbim/bim/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index 712fc73cdb..5290b1215f 100644 --- a/src/blenderbim/blenderbim/bim/export_ifc.py +++ b/src/blenderbim/blenderbim/bim/export_ifc.py @@ -90,7 +90,7 @@ class IfcExporter: for guid in to_delete: product = self.file.by_id(guid) IfcStore.unlink_element(product) - ifcopenshell.api.run("remove_product", self.file, **{"product": product}) + ifcopenshell.api.run("root.remove_product", self.file, **{"product": product}) def sync_edited_objects(self): for obj_name in IfcStore.edited_objs.copy(): diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index 4a3ffa2d75..eb7a298c4f 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -414,7 +414,11 @@ class IfcImporter: self.exclude_elements |= self.native_elements def is_native(self, element): - if not element.Representation or not element.Representation.Representations or element.HasOpenings: + if ( + not element.Representation + or not element.Representation.Representations + or getattr(element, "HasOpenings", None) + ): return representations = self.get_transformed_body_representations(element.Representation.Representations) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/helper.py b/src/blenderbim/blenderbim/bim/module/geometry/helper.py index eacc6a5de7..05f88b455c 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/helper.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/helper.py @@ -205,7 +205,8 @@ class Helper: for edge in bm.edges: edge_vector = edge.verts[1].co - edge.verts[0].co unshared_verts = set(edge.verts) - face_verts_set - if len(unshared_verts) == 1 and not (edge_vector.angle(profile_face.normal) - pi / 2 < 0.001): + angle_to_normal = edge_vector.angle(profile_face.normal) + if len(unshared_verts) == 1 and (angle_to_normal < 0.001 or angle_to_normal - pi < 0.001): if unshared_verts.pop() == edge.verts[1]: return [edge.verts[0].index, edge.verts[1].index] return [edge.verts[1].index, edge.verts[0].index] diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 97c579c005..19f86fa5d5 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -398,7 +398,7 @@ class UpdateMeshRepresentation(bpy.types.Operator): obj.data.BIMMeshProperties.ifc_definition_id = int(new_representation.id()) obj.data.name = f"{old_representation.ContextOfItems.id()}/{new_representation.id()}" - bpy.ops.bim.remove_representation(representation_id=old_representation.id()) + bpy.ops.bim.remove_representation(representation_id=old_representation.id(), obj=obj.name) Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) diff --git a/src/blenderbim/blenderbim/bim/module/georeference/__init__.py b/src/blenderbim/blenderbim/bim/module/georeference/__init__.py index ecb2fbdc7f..48ec27e3c9 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/__init__.py @@ -5,8 +5,10 @@ classes = ( operator.EnableEditingGeoreferencing, operator.DisableEditingGeoreferencing, operator.EditGeoreferencing, - operator.SetNorthOffset, - operator.GetNorthOffset, + operator.SetIfcGridNorth, + operator.SetBlenderGridNorth, + operator.SetIfcTrueNorth, + operator.SetBlenderTrueNorth, operator.RemoveGeoreferencing, operator.AddGeoreferencing, operator.ConvertLocalToGlobal, diff --git a/src/blenderbim/blenderbim/bim/module/georeference/operator.py b/src/blenderbim/blenderbim/bim/module/georeference/operator.py index c8ec61e865..4d0979bfd0 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/operator.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/operator.py @@ -17,27 +17,6 @@ class EnableEditingGeoreferencing(bpy.types.Operator): self.file = IfcStore.get_file() props = context.scene.BIMGeoreferenceProperties - while len(props.map_conversion) > 0: - props.map_conversion.remove(0) - - for attribute in IfcStore.get_schema().declaration_by_name("IfcMapConversion").all_attributes(): - data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity": - continue - new = props.map_conversion.add() - new.name = attribute.name() - new.is_null = Data.map_conversion[attribute.name()] is None - new.is_optional = attribute.optional() - new.data_type = data_type - if data_type == "string": - new.string_value = "" if new.is_null else Data.map_conversion[attribute.name()] - elif data_type == "float": - new.float_value = 0.0 if new.is_null else Data.map_conversion[attribute.name()] - elif data_type == "integer": - new.int_value = 0 if new.is_null else Data.map_conversion[attribute.name()] - elif data_type == "boolean": - new.bool_value = False if new.is_null else Data.map_conversion[attribute.name()] - while len(props.projected_crs) > 0: props.projected_crs.remove(0) @@ -69,6 +48,27 @@ class EnableEditingGeoreferencing(bpy.types.Operator): elif props.map_unit_type == "IfcConversionBasedUnit": props.map_unit_imperial = Data.projected_crs["MapUnit"]["Name"] + while len(props.map_conversion) > 0: + props.map_conversion.remove(0) + + for attribute in IfcStore.get_schema().declaration_by_name("IfcMapConversion").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity" or data_type == "select": + continue + print(attribute.name(), data_type) + new = props.map_conversion.add() + new.name = attribute.name() + new.is_null = Data.map_conversion[attribute.name()] is None + new.is_optional = attribute.optional() + # Enforce a string data type to prevent data loss in singpe-precision Blender props + new.data_type = "string" + new.string_value = "" if new.is_null else str(Data.map_conversion[attribute.name()]) + + props.has_true_north = bool(Data.true_north) + if Data.true_north: + props.true_north_abscissa = str(Data.true_north[0]) + props.true_north_ordinate = str(Data.true_north[1]) + props.is_editing = True return {"FINISHED"} @@ -91,23 +91,6 @@ class EditGeoreferencing(bpy.types.Operator): self.file = IfcStore.get_file() props = context.scene.BIMGeoreferenceProperties - map_conversion = {} - for attribute in IfcStore.get_schema().declaration_by_name("IfcMapConversion").all_attributes(): - data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity": - continue - blender_attribute = props.map_conversion.get(attribute.name()) - if blender_attribute.is_null: - map_conversion[attribute.name()] = None - elif blender_attribute.data_type == "string": - map_conversion[attribute.name()] = blender_attribute.string_value - elif blender_attribute.data_type == "float": - map_conversion[attribute.name()] = blender_attribute.float_value - elif blender_attribute.data_type == "integer": - map_conversion[attribute.name()] = blender_attribute.int_value - elif blender_attribute.data_type == "boolean": - map_conversion[attribute.name()] = blender_attribute.bool_value - projected_crs = {} for attribute in IfcStore.get_schema().declaration_by_name("IfcProjectedCRS").all_attributes(): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) @@ -129,38 +112,87 @@ class EditGeoreferencing(bpy.types.Operator): if not props.is_map_unit_null: map_unit = props.map_unit_si if props.map_unit_type == "IfcSIUnit" else props.map_unit_imperial + map_conversion = {} + for attribute in IfcStore.get_schema().declaration_by_name("IfcMapConversion").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity" or data_type == "select": + continue + blender_attribute = props.map_conversion.get(attribute.name()) + if blender_attribute.is_null: + map_conversion[attribute.name()] = None + elif blender_attribute.data_type == "string": + # We store our floats as string to prevent single precision data loss + map_conversion[attribute.name()] = float(blender_attribute.string_value) + + true_north = None + if props.has_true_north: + try: + true_north = [float(props.true_north_abscissa), float(props.true_north_ordinate)] + except: + pass + ifcopenshell.api.run( "georeference.edit_georeferencing", self.file, - **{"map_conversion": map_conversion, "projected_crs": projected_crs, "map_unit": map_unit} + **{ + "map_conversion": map_conversion, + "projected_crs": projected_crs, + "map_unit": map_unit, + "true_north": true_north, + } ) Data.load(IfcStore.get_file()) bpy.ops.bim.disable_editing_georeferencing() return {"FINISHED"} -class SetNorthOffset(bpy.types.Operator): - bl_idname = "bim.set_north_offset" - bl_label = "Set North Offset" +class SetBlenderGridNorth(bpy.types.Operator): + bl_idname = "bim.set_blender_grid_north" + bl_label = "Set Blender Grid North" def execute(self, context): context.scene.sun_pos_properties.north_offset = -radians( - ifcopenshell.util.geolocation.xy2angle( - context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").float_value, - context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").float_value, + ifcopenshell.util.geolocation.xaxis2angle( + float(context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").string_value), + float(context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").string_value), ) ) return {"FINISHED"} -class GetNorthOffset(bpy.types.Operator): - bl_idname = "bim.get_north_offset" - bl_label = "Get North Offset" +class SetIfcGridNorth(bpy.types.Operator): + bl_idname = "bim.set_ifc_grid_north" + bl_label = "Set IFC Grid North" def execute(self, context): x_angle = -context.scene.sun_pos_properties.north_offset - context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").float_value = cos(x_angle) - context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").float_value = sin(x_angle) + context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").string_value = str(cos(x_angle)) + context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").string_value = str(sin(x_angle)) + return {"FINISHED"} + + +class SetBlenderTrueNorth(bpy.types.Operator): + bl_idname = "bim.set_blender_true_north" + bl_label = "Set Blender True North" + + def execute(self, context): + context.scene.sun_pos_properties.north_offset = -radians( + ifcopenshell.util.geolocation.yaxis2angle( + float(context.scene.BIMGeoreferenceProperties.true_north_abscissa), + float(context.scene.BIMGeoreferenceProperties.true_north_ordinate), + ) + ) + return {"FINISHED"} + + +class SetIfcTrueNorth(bpy.types.Operator): + bl_idname = "bim.set_ifc_true_north" + bl_label = "Set IFC True North" + + def execute(self, context): + y_angle = -context.scene.sun_pos_properties.north_offset + radians(90) + context.scene.BIMGeoreferenceProperties.true_north_abscissa = str(cos(y_angle)) + context.scene.BIMGeoreferenceProperties.true_north_ordinate = str(sin(y_angle)) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/georeference/prop.py b/src/blenderbim/blenderbim/bim/module/georeference/prop.py index 9c7db5a841..0bcaafdc7d 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/prop.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/prop.py @@ -46,3 +46,6 @@ class BIMGeoreferenceProperties(PropertyGroup): blender_orthogonal_height: StringProperty(name="Blender Orthogonal Height", default="0") blender_x_axis_abscissa: StringProperty(name="Blender X Axis Abscissa", default="1") blender_x_axis_ordinate: StringProperty(name="Blender X Axis Ordinate", default="0") + has_true_north: BoolProperty(name="Has True North", default=True) + true_north_abscissa: StringProperty(name="True North Abscissa") + true_north_ordinate: StringProperty(name="True North Ordinate") diff --git a/src/blenderbim/blenderbim/bim/module/georeference/ui.py b/src/blenderbim/blenderbim/bim/module/georeference/ui.py index ddf04c40ff..163053f83b 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/ui.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/ui.py @@ -1,7 +1,9 @@ +import ifcopenshell.util.geolocation from bpy.types import Panel from ifcopenshell.api.georeference.data import Data from blenderbim.bim.ifc import IfcStore + class BIM_PT_gis(Panel): bl_label = "IFC Georeferencing" bl_idname = "BIM_PT_gis" @@ -15,6 +17,8 @@ class BIM_PT_gis(Panel): return IfcStore.get_file() def draw(self, context): + self.layout.use_property_split = True + self.layout.use_property_decorate = False props = context.scene.BIMGeoreferenceProperties if not Data.is_loaded: Data.load(IfcStore.get_file()) @@ -26,30 +30,10 @@ class BIM_PT_gis(Panel): def draw_editable_ui(self, context): props = context.scene.BIMGeoreferenceProperties row = self.layout.row(align=True) - row.label(text="Map Conversion", icon="GRID") + row.label(text="Projected CRS", icon="WORLD") row.operator("bim.edit_georeferencing", icon="CHECKMARK", text="") row.operator("bim.disable_editing_georeferencing", icon="X", text="") - for attribute in props.map_conversion: - if attribute.name == "XAxisAbscissa" and hasattr(context.scene, "sun_pos_properties"): - row = self.layout.row(align=True) - row.operator("bim.get_north_offset", text="Set IFC North") - row.operator("bim.set_north_offset", text="Set Blender North") - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") - - row = self.layout.row(align=True) - row.label(text="Projected CRS", icon="WORLD") - for attribute in props.projected_crs: row = self.layout.row(align=True) if attribute.data_type == "string": @@ -70,10 +54,43 @@ class BIM_PT_gis(Panel): row.prop(props, "map_unit_imperial", text="") row.prop(props, "is_map_unit_null", icon="RADIOBUT_OFF" if props.is_map_unit_null else "RADIOBUT_ON", text="") + row = self.layout.row() + row.label(text="Map Conversion", icon="GRID") + + for attribute in props.map_conversion: + if attribute.name == "Scale" and hasattr(context.scene, "sun_pos_properties"): + row = self.layout.row(align=True) + row.operator("bim.set_ifc_grid_north", text="Set IFC North") + row.operator("bim.set_blender_grid_north", text="Set Blender North") + row = self.layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "integer": + row.prop(attribute, "int_value", text=attribute.name) + elif attribute.data_type == "float": + row.prop(attribute, "float_value", text=attribute.name) + elif attribute.data_type == "boolean": + row.prop(attribute, "bool_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + + row = self.layout.row() + row.label(text="True North", icon="LIGHT_SUN") + row = self.layout.row() + row.prop(props, "has_true_north") + row = self.layout.row() + row.prop(props, "true_north_abscissa") + row = self.layout.row() + row.prop(props, "true_north_ordinate") + if hasattr(context.scene, "sun_pos_properties"): + row = self.layout.row(align=True) + row.operator("bim.set_ifc_true_north", text="Set IFC North") + row.operator("bim.set_blender_true_north", text="Set Blender North") + def draw_ui(self, context): props = context.scene.BIMGeoreferenceProperties - if not Data.map_conversion and IfcStore.get_file().schema != "IFC2X3": + if not Data.projected_crs and IfcStore.get_file().schema != "IFC2X3": row = self.layout.row(align=True) row.label(text="Not Georeferenced") row.operator("bim.add_georeferencing", icon="ADD", text="") @@ -100,26 +117,26 @@ class BIM_PT_gis(Panel): row = self.layout.row(align=True) row.label(text="XAxisOrdinate") row.label(text=props.blender_x_axis_ordinate) + row.label(text="Derived Grid North") + row.label( + text=str( + round( + ifcopenshell.util.geolocation.xaxis2angle( + float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate) + ), + 3, + ) + ) + ) elif IfcStore.get_file().schema == "IFC2X3": row = self.layout.row() row.label(text="Not Georeferenced") - if Data.map_conversion: - row = self.layout.row(align=True) - row.label(text="Map Conversion", icon="GRID") - row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="") - row.operator("bim.remove_georeferencing", icon="X", text="") - - for key, value in Data.map_conversion.items(): - if key == "id" or key == "type" or key == "SourceCRS" or key == "TargetCRS" or not value: - continue - row = self.layout.row(align=True) - row.label(text=key) - row.label(text=str(value)) - if Data.projected_crs: row = self.layout.row(align=True) row.label(text="Projected CRS", icon="WORLD") + row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="") + row.operator("bim.remove_georeferencing", icon="X", text="") for key, value in Data.projected_crs.items(): if key == "id" or key == "type" or not value: @@ -132,6 +149,40 @@ class BIM_PT_gis(Panel): row.label(text=key) row.label(text=str(value)) + if Data.map_conversion: + row = self.layout.row(align=True) + row.label(text="Map Conversion", icon="GRID") + + for key, value in Data.map_conversion.items(): + if key == "id" or key == "type" or key == "SourceCRS" or key == "TargetCRS" or value is None: + continue + row = self.layout.row(align=True) + row.label(text=key) + row.label(text=str(value)) + if key == "XAxisOrdinate": + row = self.layout.row(align=True) + row.label(text="Derived Angle") + row.label( + text=str( + round( + ifcopenshell.util.geolocation.xaxis2angle( + Data.map_conversion["XAxisAbscissa"], Data.map_conversion["XAxisOrdinate"] + ), + 3, + ) + ) + ) + + if Data.true_north: + row = self.layout.row() + row.label(text="True North", icon="LIGHT_SUN") + row = self.layout.row(align=True) + row.label(text="Vector") + row.label(text=str(Data.true_north[0:2])[1:-1]) + row = self.layout.row(align=True) + row.label(text="Derived Angle") + row.label(text=str(round(ifcopenshell.util.geolocation.yaxis2angle(*Data.true_north[0:2]), 3))) + class BIM_PT_gis_utilities(Panel): bl_idname = "BIM_PT_gis_utilities" diff --git a/src/blenderbim/blenderbim/bim/module/layer/operator.py b/src/blenderbim/blenderbim/bim/module/layer/operator.py index 7c930823e9..52e5a9760d 100644 --- a/src/blenderbim/blenderbim/bim/module/layer/operator.py +++ b/src/blenderbim/blenderbim/bim/module/layer/operator.py @@ -47,7 +47,7 @@ class EnableEditingLayer(bpy.types.Operator): for attribute in IfcStore.get_schema().declaration_by_name("IfcPresentationLayerAssignment").all_attributes(): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity": + if data_type == "entity" or data_type == "select": continue new = props.layer_attributes.add() new.name = attribute.name() diff --git a/src/blenderbim/blenderbim/bim/module/material/__init__.py b/src/blenderbim/blenderbim/bim/module/material/__init__.py index 89e1f74243..83ef17c184 100644 --- a/src/blenderbim/blenderbim/bim/module/material/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/material/__init__.py @@ -8,6 +8,9 @@ classes = ( operator.UnassignMaterial, operator.AddConstituent, operator.RemoveConstituent, + operator.AddProfile, + operator.RemoveProfile, + operator.AssignParameterizedProfile, operator.AddLayer, operator.RemoveLayer, operator.ReorderMaterialSetItem, diff --git a/src/blenderbim/blenderbim/bim/module/material/operator.py b/src/blenderbim/blenderbim/bim/module/material/operator.py index bf7dde4867..b5ff702d02 100644 --- a/src/blenderbim/blenderbim/bim/module/material/operator.py +++ b/src/blenderbim/blenderbim/bim/module/material/operator.py @@ -1,8 +1,36 @@ import bpy +import json import ifcopenshell.api import ifcopenshell.util.attribute from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.material.data import Data +from ifcopenshell.api.profile.data import Data as ProfileData + + +class AssignParameterizedProfile(bpy.types.Operator): + bl_idname = "bim.assign_parameterized_profile" + bl_label = "Assign Parameterized Profile" + ifc_class: bpy.props.StringProperty() + material_profile: bpy.props.IntProperty() + obj: bpy.props.StringProperty() + + def execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + self.file = IfcStore.get_file() + profile = ifcopenshell.api.run( + "profile.add_parameterized_profile", + self.file, + **{"ifc_class": self.ifc_class}, + ) + ifcopenshell.api.run( + "material.assign_profile", + self.file, + **{"material_profile": self.file.by_id(self.material_profile), "profile": profile} + ) + Data.load_profiles() + ProfileData.load(self.file) + bpy.ops.bim.enable_editing_material_set_item(obj=obj.name, material_set_item=self.material_profile) + return {"FINISHED"} class AddMaterial(bpy.types.Operator): @@ -115,6 +143,43 @@ class RemoveConstituent(bpy.types.Operator): return {"FINISHED"} +class AddProfile(bpy.types.Operator): + bl_idname = "bim.add_profile" + bl_label = "Add Profile" + obj: bpy.props.StringProperty() + profile_set: bpy.props.IntProperty() + + def execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "material.add_profile", + self.file, + **{ + "profile_set": self.file.by_id(self.profile_set), + "material": self.file.by_id(int(obj.BIMObjectMaterialProperties.material)), + }, + ) + Data.load_profiles() + return {"FINISHED"} + + +class RemoveProfile(bpy.types.Operator): + bl_idname = "bim.remove_profile" + bl_label = "Remove Profile" + obj: bpy.props.StringProperty() + profile: bpy.props.IntProperty() + + def execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "material.remove_profile", self.file, **{"profile": self.file.by_id(self.profile)} + ) + Data.load_profiles() + return {"FINISHED"} + + class AddLayer(bpy.types.Operator): bl_idname = "bim.add_layer" bl_label = "Add Layer" @@ -333,8 +398,8 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object - props = obj.BIMObjectMaterialProperties - props.active_material_set_item_id = self.material_set_item + self.props = obj.BIMObjectMaterialProperties + self.props.active_material_set_item_id = self.material_set_item product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id] material_set_item = self.file.by_id(self.material_set_item) @@ -347,17 +412,24 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): else: material_set_item_data = {} - props.material_set_item_material = str(material_set_item_data["Material"]) + self.props.material_set_item_material = str(material_set_item_data["Material"]) - while len(props.material_set_item_attributes) > 0: - props.material_set_item_attributes.remove(0) + self.load_set_item_attributes(material_set_item, material_set_item_data) + if material_set_item.is_a("IfcMaterialProfile"): + self.load_profile_attributes(material_set_item, material_set_item_data) + + return {"FINISHED"} + + def load_set_item_attributes(self, material_set_item, material_set_item_data): + while len(self.props.material_set_item_attributes) > 0: + self.props.material_set_item_attributes.remove(0) for attribute in IfcStore.get_schema().declaration_by_name(material_set_item.is_a()).all_attributes(): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) if data_type == "entity": continue if attribute.name() in material_set_item_data: - new = props.material_set_item_attributes.add() + new = self.props.material_set_item_attributes.add() new.name = attribute.name() new.is_null = material_set_item_data[attribute.name()] is None new.data_type = data_type @@ -369,7 +441,45 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): new.int_value = 0 if new.is_null else material_set_item_data[attribute.name()] elif data_type == "boolean": new.bool_value = False if new.is_null else material_set_item_data[attribute.name()] - return {"FINISHED"} + + def load_profile_attributes(self, material_set_item, material_set_item_data): + while len(self.props.material_set_item_profile_attributes) > 0: + self.props.material_set_item_profile_attributes.remove(0) + + if not material_set_item_data["Profile"]: + return + + profile = self.file.by_id(material_set_item_data["Profile"]) + profile_data = ProfileData.profiles[material_set_item_data["Profile"]] + + for attribute in IfcStore.get_schema().declaration_by_name(profile.is_a()).all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + if attribute.name() in profile_data: + new = self.props.material_set_item_profile_attributes.add() + new.name = attribute.name() + new.is_null = profile_data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if data_type == "string": + new.string_value = "" if new.is_null else profile_data[attribute.name()] + elif data_type == "float": + new.float_value = 0.0 if new.is_null else profile_data[attribute.name()] + elif data_type == "integer": + new.int_value = 0 if new.is_null else profile_data[attribute.name()] + elif data_type == "boolean": + new.bool_value = False if new.is_null else profile_data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if profile_data[attribute.name()]: + new.enum_value = profile_data[attribute.name()] + + # Force null to be false if the attribute is mandatory because when we first assign a profile, all of + # its fields are null (which is illegal). + # TODO: find a better solution. + if not new.is_optional: + new.is_null = False class DisableEditingMaterialSetItem(bpy.types.Operator): @@ -431,16 +541,31 @@ class EditMaterialSetItem(bpy.types.Operator): ) Data.load_layers() elif product_data["type"] == "IfcMaterialProfileSet": + profile_attributes = {} + for attribute in props.material_set_item_profile_attributes: + if attribute.data_type == "string": + value = attribute.string_value + elif attribute.data_type == "float": + value = attribute.float_value + elif attribute.data_type == "integer": + value = attribute.int_value + elif attribute.data_type == "boolean": + value = attribute.bool_value + elif attribute.data_type == "enum": + value = attribute.enum_value + profile_attributes[attribute.name] = None if attribute.is_null else value ifcopenshell.api.run( "material.edit_profile", self.file, **{ "profile": self.file.by_id(self.material_set_item), "attributes": attributes, + "profile_attributes": profile_attributes, "material": self.file.by_id(int(obj.BIMObjectMaterialProperties.material_set_item_material)), }, ) Data.load_profiles() + ProfileData.load(self.file) else: pass diff --git a/src/blenderbim/blenderbim/bim/module/material/prop.py b/src/blenderbim/blenderbim/bim/module/material/prop.py index 4795832bbc..ffa7c5d87a 100644 --- a/src/blenderbim/blenderbim/bim/module/material/prop.py +++ b/src/blenderbim/blenderbim/bim/module/material/prop.py @@ -1,5 +1,5 @@ import bpy -import blenderbim.bim.schema # refactor +import blenderbim.bim.schema # refactor from ifcopenshell.api.material.data import Data from blenderbim.bim.ifc import IfcStore from blenderbim.bim.prop import StrProperty, Attribute @@ -17,6 +17,29 @@ from bpy.props import ( materials_enum = [] materialtypes_enum = [] +profileclasses_enum = [] +parameterizedprofileclasses_enum = [] + + +def getProfileClasses(self, context): + global profileclasses_enum + if len(profileclasses_enum) == 0 and IfcStore.get_schema(): + profileclasses_enum.clear() + profileclasses_enum = [ + (t.name(), t.name(), "") for t in IfcStore.get_schema().declaration_by_name("IfcProfileDef").subtypes() + ] + return profileclasses_enum + + +def getParameterizedProfileClasses(self, context): + global parameterizedprofileclasses_enum + if len(parameterizedprofileclasses_enum) == 0 and IfcStore.get_schema(): + parameterizedprofileclasses_enum.clear() + parameterizedprofileclasses_enum = [ + (t.name(), t.name(), "") + for t in IfcStore.get_schema().declaration_by_name("IfcParameterizedProfileDef").subtypes() + ] + return parameterizedprofileclasses_enum def getMaterials(self, context): @@ -52,4 +75,9 @@ class BIMObjectMaterialProperties(PropertyGroup): material_set_attributes: CollectionProperty(name="Material Set Attributes", type=Attribute) active_material_set_item_id: IntProperty(name="Active Material Set ID") material_set_item_attributes: CollectionProperty(name="Material Set Item Attributes", type=Attribute) + material_set_item_profile_attributes: CollectionProperty(name="Material Set Item Profile Attributes", type=Attribute) material_set_item_material: EnumProperty(items=getMaterials, name="Material") + profile_classes: EnumProperty(items=getProfileClasses, name="Profile Classes") + parameterized_profile_classes: EnumProperty( + items=getParameterizedProfileClasses, name="Parameterized Profile Classes" + ) diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py index 6d432f673a..591fa8ccb3 100644 --- a/src/blenderbim/blenderbim/bim/module/material/ui.py +++ b/src/blenderbim/blenderbim/bim/module/material/ui.py @@ -1,5 +1,6 @@ from bpy.types import Panel from ifcopenshell.api.material.data import Data +from ifcopenshell.api.profile.data import Data as ProfileData from blenderbim.bim.ifc import IfcStore @@ -46,6 +47,8 @@ class BIM_PT_object_material(Panel): Data.load(IfcStore.get_file()) if self.oprops.ifc_definition_id not in Data.products: Data.load(IfcStore.get_file(), self.oprops.ifc_definition_id) + if not ProfileData.is_loaded: + ProfileData.load(self.file) self.product_data = Data.products[self.oprops.ifc_definition_id] if not Data.materials: @@ -172,6 +175,39 @@ class BIM_PT_object_material(Panel): row.prop(attribute, "bool_value", text=attribute.name) row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + if self.set_item_name == "profile": + self.draw_assign_profile_ui(box, item) + self.draw_editable_profile_ui(box, item) + + def draw_assign_profile_ui(self, layout, item): + row = layout.row(align=True) + row.prop(self.props, "profile_classes", text="") + if self.props.profile_classes == "IfcParameterizedProfileDef": + row.prop(self.props, "parameterized_profile_classes", text="") + op = row.operator("bim.assign_parameterized_profile", icon="GREASEPENCIL" if item["Profile"] else "ADD", text="") + op.ifc_class = self.props.parameterized_profile_classes + op.material_profile = item["id"] + else: + # TODO: support non parametric profiles by showing a list of named profiles to select from, or an + # eyedropper to pick profile geometry from the scene + row.operator("bim.disable_editing_material_set_item", icon="X", text="") + + def draw_editable_profile_ui(self, layout, item): + for attribute in self.props.material_set_item_profile_attributes: + row = layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "integer": + row.prop(attribute, "int_value", text=attribute.name) + elif attribute.data_type == "float": + row.prop(attribute, "float_value", text=attribute.name) + elif attribute.data_type == "boolean": + row.prop(attribute, "bool_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + def draw_read_only_set_item_ui(self, set_item_id, index, is_first=False, is_last=False): if self.product_data["type"] == "IfcMaterialList": item = Data.materials[set_item_id] diff --git a/src/blenderbim/blenderbim/bim/module/model/pie.py b/src/blenderbim/blenderbim/bim/module/model/pie.py index a93010bd47..b19a258f4c 100644 --- a/src/blenderbim/blenderbim/bim/module/model/pie.py +++ b/src/blenderbim/blenderbim/bim/module/model/pie.py @@ -84,6 +84,8 @@ class PieAddOpening(bpy.types.Operator): for obj in context.selected_objects: if "IfcOpeningElement" in obj.name or not obj.BIMObjectProperties.ifc_definition_id: opening_name = obj.name + elif len(obj.children) == 1 and not obj.children[0].BIMObjectProperties.ifc_definition_id: + opening_name = obj.children[0].name else: opj_name = obj.name bpy.ops.bim.add_opening(obj=opj_name, opening=opening_name) diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index 846f082a61..fb41b6e9ed 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -3,6 +3,7 @@ from . import ui, prop, operator classes = ( operator.CreateProject, + operator.CreateProjectLibrary, operator.ValidateIfcFile, prop.BIMProjectProperties, ui.BIM_PT_project, diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index 6b56cd45cf..60baeea3a0 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -53,6 +53,30 @@ class CreateProject(bpy.types.Operator): return {"FINISHED"} +class CreateProjectLibrary(bpy.types.Operator): + bl_idname = "bim.create_project_library" + bl_label = "Create Project Library" + + def execute(self, context): + self.file = IfcStore.get_file() + if self.file: + return {"FINISHED"} + + IfcStore.file = ifcopenshell.api.run( + "project.create_file", **{"version": bpy.context.scene.BIMProperties.export_schema} + ) + self.file = IfcStore.get_file() + + if self.file.schema == "IFC2X3": + bpy.ops.bim.add_person() + bpy.ops.bim.add_organisation() + + project_library = bpy.data.objects.new("My Project Library", None) + bpy.ops.bim.assign_class(obj=project_library.name, ifc_class="IfcProjectLibrary") + bpy.ops.bim.assign_unit() + return {"FINISHED"} + + class ValidateIfcFile(bpy.types.Operator): bl_idname = "bim.validate_ifc_file" bl_label = "Validate IFC File" diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 1da1f0f64d..7e255ea0d3 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -63,3 +63,6 @@ class BIM_PT_project(Panel): row.prop(props, "volume_unit", text="Volume Unit") row = self.layout.row() row.operator("bim.create_project") + if props.export_schema != "IFC2X3": + row = self.layout.row() + row.operator("bim.create_project_library") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 26a02991c2..16af753c26 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -2,21 +2,55 @@ import bpy from . import ui, prop, operator classes = ( + operator.LoadWorkPlans, + operator.DisableWorkPlanEditingUI, + operator.AddWorkPlan, + operator.EditWorkPlan, + operator.RemoveWorkPlan, + operator.EnableEditingWorkPlan, + operator.DisableEditingWorkPlan, + operator.LoadWorkSchedules, + operator.DisableWorkScheduleEditingUI, + operator.AddWorkSchedule, + operator.EditWorkSchedule, + operator.RemoveWorkSchedule, + operator.EnableEditingWorkSchedule, + operator.DisableEditingWorkSchedule, operator.LoadTasks, operator.DisableTaskEditingUI, - operator.AddWorkPlan, - operator.RemoveWorkPlan, + operator.LoadWorkCalendars, + operator.DisableWorkCalendarEditingUI, + operator.AddWorkCalendar, + operator.EditWorkCalendar, + operator.RemoveWorkCalendar, + operator.EnableEditingWorkCalendar, + operator.DisableEditingWorkCalendar, + prop.WorkPlan, + prop.BIMWorkPlanProperties, + prop.WorkSchedule, + prop.BIMWorkScheduleProperties, + prop.WorkCalendar, + prop.BIMWorkCalendarProperties, prop.Task, prop.BIMTaskProperties, ui.BIM_PT_work_plans, + ui.BIM_UL_work_plans, + ui.BIM_PT_work_schedules, + ui.BIM_UL_work_schedules, + ui.BIM_PT_work_calendars, + ui.BIM_UL_work_calendars, ui.BIM_PT_tasks, ui.BIM_UL_tasks, ) - def register(): bpy.types.Scene.BIMTaskProperties = bpy.props.PointerProperty(type=prop.BIMTaskProperties) - + bpy.types.Scene.BIMWorkPlanProperties = bpy.props.PointerProperty(type=prop.BIMWorkPlanProperties) + bpy.types.Scene.BIMWorkScheduleProperties = bpy.props.PointerProperty(type=prop.BIMWorkScheduleProperties) + bpy.types.Scene.BIMWorkCalendarProperties = bpy.props.PointerProperty(type=prop.BIMWorkCalendarProperties) def unregister(): del bpy.types.Scene.BIMTaskProperties + del bpy.types.Scene.BIMWorkPlanProperties + del bpy.types.Scene.BIMWorkScheduleProperties + del bpy.types.Scene.BIMWorkCalendarProperties diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 8fb1570267..f78fbf5513 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1,9 +1,36 @@ import bpy +import json import ifcopenshell.api from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.sequence.data import Data +class LoadWorkPlans(bpy.types.Operator): + bl_idname = "bim.load_work_plans" + bl_label = "Load Work Plans" + + def execute(self, context): + props = context.scene.BIMWorkPlanProperties + while len(props.work_plans) > 0: + props.work_plans.remove(0) + for ifc_definition_id, work_plan in Data.work_plans.items(): + new = props.work_plans.add() + new.ifc_definition_id = ifc_definition_id + new.name = work_plan["Name"] or "Unnamed" + props.is_editing = True + bpy.ops.bim.disable_editing_work_plan() + return {"FINISHED"} + + +class DisableWorkPlanEditingUI(bpy.types.Operator): + bl_idname = "bim.disable_work_plan_editing_ui" + bl_label = "Disable WorkPlan Editing UI" + + def execute(self, context): + context.scene.BIMWorkPlanProperties.is_editing = False + return {"FINISHED"} + + class AddWorkPlan(bpy.types.Operator): bl_idname = "bim.add_work_plan" bl_label = "Add Work Plan" @@ -11,6 +38,33 @@ class AddWorkPlan(bpy.types.Operator): def execute(self, context): ifcopenshell.api.run("sequence.add_work_plan", IfcStore.get_file()) Data.load(IfcStore.get_file()) + bpy.ops.bim.load_work_plans() + return {"FINISHED"} + + +class EditWorkPlan(bpy.types.Operator): + bl_idname = "bim.edit_work_plan" + bl_label = "Edit Work Plan" + + def execute(self, context): + props = context.scene.BIMWorkPlanProperties + attributes = {} + for attribute in props.work_plan_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + if attribute.data_type == "string": + attributes[attribute.name] = attribute.string_value + elif attribute.data_type == "enum": + attributes[attribute.name] = attribute.enum_value + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.edit_work_plan", + self.file, + **{"work_plan": self.file.by_id(props.active_work_plan_id), "attributes": attributes} + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_work_plans() return {"FINISHED"} @@ -20,10 +74,290 @@ class RemoveWorkPlan(bpy.types.Operator): work_plan: bpy.props.IntProperty() def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run("sequence.remove_work_plan", self.file, **{"work_plan": self.file.by_id(self.work_plan)}) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_work_plans() + return {"FINISHED"} + + +class EnableEditingWorkPlan(bpy.types.Operator): + bl_idname = "bim.enable_editing_work_plan" + bl_label = "Enable Editing Work Plan" + work_plan: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkPlanProperties + while len(props.work_plan_attributes) > 0: + props.work_plan_attributes.remove(0) + + data = Data.work_plans[self.work_plan] + + for attribute in IfcStore.get_schema().declaration_by_name("IfcWorkPlan").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + new = props.work_plan_attributes.add() + new.name = attribute.name() + new.is_null = data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if attribute.name() in ["CreationDate", "StartTime", "FinishTime"]: + new.string_value = "" if new.is_null else data[attribute.name()].isoformat() + elif data_type == "string": + new.string_value = "" if new.is_null else data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if data[attribute.name()]: + new.enum_value = data[attribute.name()] + props.active_work_plan_id = self.work_plan + return {"FINISHED"} + + +class DisableEditingWorkPlan(bpy.types.Operator): + bl_idname = "bim.disable_editing_work_plan" + bl_label = "Disable Editing Work Plan" + + def execute(self, context): + context.scene.BIMWorkPlanProperties.active_work_plan_id = 0 + return {"FINISHED"} + + +class LoadWorkSchedules(bpy.types.Operator): + bl_idname = "bim.load_work_schedules" + bl_label = "Load Work Schedules" + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + while len(props.work_schedules) > 0: + props.work_schedules.remove(0) + for ifc_definition_id, work_schedule in Data.work_schedules.items(): + new = props.work_schedules.add() + new.ifc_definition_id = ifc_definition_id + new.name = work_schedule["Name"] or "Unnamed" + props.is_editing = True + bpy.ops.bim.disable_editing_work_schedule() + return {"FINISHED"} + + +class DisableWorkScheduleEditingUI(bpy.types.Operator): + bl_idname = "bim.disable_work_schedule_editing_ui" + bl_label = "Disable WorkSchedule Editing UI" + + def execute(self, context): + context.scene.BIMWorkScheduleProperties.is_editing = False + return {"FINISHED"} + + +class AddWorkSchedule(bpy.types.Operator): + bl_idname = "bim.add_work_schedule" + bl_label = "Add Work Schedule" + + def execute(self, context): + ifcopenshell.api.run("sequence.add_work_schedule", IfcStore.get_file()) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_work_schedules() + return {"FINISHED"} + + +class EditWorkSchedule(bpy.types.Operator): + bl_idname = "bim.edit_work_schedule" + bl_label = "Edit Work Schedule" + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + attributes = {} + for attribute in props.work_schedule_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + if attribute.data_type == "string": + attributes[attribute.name] = attribute.string_value + elif attribute.data_type == "enum": + attributes[attribute.name] = attribute.enum_value + self.file = IfcStore.get_file() ifcopenshell.api.run( - "sequence.remove_work_plan", IfcStore.get_file(), work_plan=IfcStore.get_file().by_id(self.work_plan) + "sequence.edit_work_schedule", + self.file, + **{"work_schedule": self.file.by_id(props.active_work_schedule_id), "attributes": attributes} ) Data.load(IfcStore.get_file()) + bpy.ops.bim.load_work_schedules() + return {"FINISHED"} + + +class RemoveWorkSchedule(bpy.types.Operator): + bl_idname = "bim.remove_work_schedule" + bl_label = "Remove Work Schedule" + work_schedule: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.remove_work_schedule", self.file, **{"work_schedule": self.file.by_id(self.work_schedule)} + ) + Data.load(self.file) + bpy.ops.bim.load_work_schedules() + return {"FINISHED"} + + +class EnableEditingWorkSchedule(bpy.types.Operator): + bl_idname = "bim.enable_editing_work_schedule" + bl_label = "Enable Editing Work Schedule" + work_schedule: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + while len(props.work_schedule_attributes) > 0: + props.work_schedule_attributes.remove(0) + + data = Data.work_schedules[self.work_schedule] + + for attribute in IfcStore.get_schema().declaration_by_name("IfcWorkSchedule").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + new = props.work_schedule_attributes.add() + new.name = attribute.name() + new.is_null = data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if attribute.name() in ["CreationDate", "StartTime", "FinishTime"]: + new.string_value = "" if new.is_null else data[attribute.name()].isoformat() + elif data_type == "string": + new.string_value = "" if new.is_null else data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if data[attribute.name()]: + new.enum_value = data[attribute.name()] + props.active_work_schedule_id = self.work_schedule + return {"FINISHED"} + + +class DisableEditingWorkSchedule(bpy.types.Operator): + bl_idname = "bim.disable_editing_work_schedule" + bl_label = "Disable Editing Work Schedule" + + def execute(self, context): + context.scene.BIMWorkScheduleProperties.active_work_schedule_id = 0 + return {"FINISHED"} + + +class LoadWorkCalendars(bpy.types.Operator): + bl_idname = "bim.load_work_calendars" + bl_label = "Load Work Calendars" + + def execute(self, context): + props = context.scene.BIMWorkCalendarProperties + while len(props.work_calendars) > 0: + props.work_calendars.remove(0) + for ifc_definition_id, work_calendar in Data.work_calendars.items(): + new = props.work_calendars.add() + new.ifc_definition_id = ifc_definition_id + new.name = work_calendar["Name"] or "Unnamed" + props.is_editing = True + bpy.ops.bim.disable_editing_work_calendar() + return {"FINISHED"} + + +class DisableWorkCalendarEditingUI(bpy.types.Operator): + bl_idname = "bim.disable_work_calendar_editing_ui" + bl_label = "Disable WorkCalendar Editing UI" + + def execute(self, context): + context.scene.BIMWorkCalendarProperties.is_editing = False + return {"FINISHED"} + + +class AddWorkCalendar(bpy.types.Operator): + bl_idname = "bim.add_work_calendar" + bl_label = "Add Work Calendar" + + def execute(self, context): + ifcopenshell.api.run("sequence.add_work_calendar", IfcStore.get_file()) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_work_calendars() + return {"FINISHED"} + + +class EditWorkCalendar(bpy.types.Operator): + bl_idname = "bim.edit_work_calendar" + bl_label = "Edit Work Calendar" + + def execute(self, context): + props = context.scene.BIMWorkCalendarProperties + attributes = {} + for attribute in props.work_calendar_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + if attribute.data_type == "string": + attributes[attribute.name] = attribute.string_value + elif attribute.data_type == "enum": + attributes[attribute.name] = attribute.enum_value + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.edit_work_calendar", + self.file, + **{"work_calendar": self.file.by_id(props.active_work_calendar_id), "attributes": attributes} + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_work_calendars() + return {"FINISHED"} + + +class RemoveWorkCalendar(bpy.types.Operator): + bl_idname = "bim.remove_work_calendar" + bl_label = "Remove Work Plan" + work_calendar: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.remove_work_calendar", self.file, **{"work_calendar": self.file.by_id(self.work_calendar)} + ) + Data.load(self.file) + bpy.ops.bim.load_work_calendars() + return {"FINISHED"} + + +class EnableEditingWorkCalendar(bpy.types.Operator): + bl_idname = "bim.enable_editing_work_calendar" + bl_label = "Enable Editing Work Plan" + work_calendar: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkCalendarProperties + while len(props.work_calendar_attributes) > 0: + props.work_calendar_attributes.remove(0) + + data = Data.work_calendars[self.work_calendar] + + for attribute in IfcStore.get_schema().declaration_by_name("IfcWorkCalendar").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + new = props.work_calendar_attributes.add() + new.name = attribute.name() + new.is_null = data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if data_type == "string": + new.string_value = "" if new.is_null else data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if data[attribute.name()]: + new.enum_value = data[attribute.name()] + props.active_work_calendar_id = self.work_calendar + return {"FINISHED"} + + +class DisableEditingWorkCalendar(bpy.types.Operator): + bl_idname = "bim.disable_editing_work_calendar" + bl_label = "Disable Editing Work Calendar" + + def execute(self, context): + context.scene.BIMWorkCalendarProperties.active_work_calendar_id = 0 return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 226ad5fea0..29a1af6b14 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -23,3 +23,42 @@ class BIMTaskProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing", default=False) tasks: CollectionProperty(name="Tasks", type=Task) active_task_index: IntProperty(name="Active Task Index") + + +class WorkPlan(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + + +class BIMWorkPlanProperties(PropertyGroup): + work_plan_attributes: CollectionProperty(name="Work Plan Attributes", type=Attribute) + is_editing: BoolProperty(name="Is Editing", default=False) + work_plans: CollectionProperty(name="Work Plans", type=WorkPlan) + active_work_plan_index: IntProperty(name="Active Work Plan Index") + active_work_plan_id: IntProperty(name="Active Work Plan Id") + + +class WorkSchedule(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + + +class BIMWorkScheduleProperties(PropertyGroup): + work_schedule_attributes: CollectionProperty(name="Work Schedule Attributes", type=Attribute) + is_editing: BoolProperty(name="Is Editing", default=False) + work_schedules: CollectionProperty(name="Work Schedules", type=WorkSchedule) + active_work_schedule_index: IntProperty(name="Active Work Schedules Index") + active_work_schedule_id: IntProperty(name="Active Work Schedules Id") + + +class WorkCalendar(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + + +class BIMWorkCalendarProperties(PropertyGroup): + work_calendar_attributes: CollectionProperty(name="Work Calendar Attributes", type=Attribute) + is_editing: BoolProperty(name="Is Editing", default=False) + work_calendars: CollectionProperty(name="Work Calendar", type=WorkCalendar) + active_work_calendar_index: IntProperty(name="Active Work Calendar Index") + active_work_calendar_id: IntProperty(name="Active Work Calendar Id") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index f38abf8161..9ed96cef3c 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -18,15 +18,181 @@ class BIM_PT_work_plans(Panel): def draw(self, context): if not Data.is_loaded: Data.load(IfcStore.get_file()) + self.props = context.scene.BIMWorkPlanProperties + row = self.layout.row(align=True) + row.label(text="{} Work Plans Found".format(len(Data.work_plans)), icon="TEXT") + if self.props.is_editing: + row.operator("bim.add_work_plan", text="", icon="ADD") + row.operator("bim.disable_work_plan_editing_ui", text="", icon="CHECKMARK") + else: + row.operator("bim.load_work_plans", text="", icon="GREASEPENCIL") - row = self.layout.row() - row.operator("bim.add_work_plan", icon="ADD") + if self.props.is_editing: + self.layout.template_list( + "BIM_UL_work_plans", + "", + self.props, + "work_plans", + self.props, + "active_work_plan_index", + ) - for work_plan_id, work_plan in Data.work_plans.items(): + if self.props.active_work_plan_id: + self.draw_editable_ui(context) + + def draw_editable_ui(self, context): + for attribute in self.props.work_plan_attributes: row = self.layout.row(align=True) - row.label(text=work_plan["Name"] or "Unnamed", icon="TEXT") - row.operator("bim.add_work_plan", text="", icon="GREASEPENCIL") - row.operator("bim.remove_work_plan", text="", icon="X").work_plan = work_plan_id + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + + +class BIM_UL_work_plans(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=item.name) + if context.scene.BIMWorkPlanProperties.active_work_plan_id == item.ifc_definition_id: + row.operator("bim.edit_work_plan", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_work_plan", text="", icon="X") + elif context.scene.BIMWorkPlanProperties.active_work_plan_id: + row.operator("bim.remove_work_plan", text="", icon="X").work_plan = item.ifc_definition_id + else: + op = row.operator("bim.enable_editing_work_plan", text="", icon="GREASEPENCIL") + op.work_plan = item.ifc_definition_id + row.operator("bim.remove_work_plan", text="", icon="X").work_plan = item.ifc_definition_id + + +class BIM_PT_work_schedules(Panel): + bl_label = "IFC Work Schedules" + bl_idname = "BIM_PT_work_schedules" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @classmethod + def poll(cls, context): + return IfcStore.get_file() + + def draw(self, context): + if not Data.is_loaded: + Data.load(IfcStore.get_file()) + self.props = context.scene.BIMWorkScheduleProperties + row = self.layout.row(align=True) + row.label(text="{} Work Schedules Found".format(len(Data.work_schedules)), icon="TEXT") + if self.props.is_editing: + row.operator("bim.add_work_schedule", text="", icon="ADD") + row.operator("bim.disable_work_schedule_editing_ui", text="", icon="CHECKMARK") + else: + row.operator("bim.load_work_schedules", text="", icon="GREASEPENCIL") + + if self.props.is_editing: + self.layout.template_list( + "BIM_UL_work_schedules", + "", + self.props, + "work_schedules", + self.props, + "active_work_schedule_index", + ) + + if self.props.active_work_schedule_id: + self.draw_editable_ui(context) + + def draw_editable_ui(self, context): + for attribute in self.props.work_schedule_attributes: + row = self.layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + + +class BIM_UL_work_schedules(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=item.name) + if context.scene.BIMWorkScheduleProperties.active_work_schedule_id == item.ifc_definition_id: + row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_work_schedule", text="", icon="X") + elif context.scene.BIMWorkScheduleProperties.active_work_schedule_id: + row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = item.ifc_definition_id + else: + op = row.operator("bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL") + op.work_schedule = item.ifc_definition_id + row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = item.ifc_definition_id + + +class BIM_PT_work_calendars(Panel): + bl_label = "IFC Work Calendars" + bl_idname = "BIM_PT_work_calendars" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @classmethod + def poll(cls, context): + return IfcStore.get_file() + + def draw(self, context): + if not Data.is_loaded: + Data.load(IfcStore.get_file()) + self.props = context.scene.BIMWorkCalendarProperties + row = self.layout.row(align=True) + row.label(text="{} Work Calendar Found".format(len(Data.work_calendars)), icon="TEXT") + if self.props.is_editing: + row.operator("bim.add_work_calendar", text="", icon="ADD") + row.operator("bim.disable_work_calendar_editing_ui", text="", icon="CHECKMARK") + else: + row.operator("bim.load_work_calendars", text="", icon="GREASEPENCIL") + + if self.props.is_editing: + self.layout.template_list( + "BIM_UL_work_calendars", + "", + self.props, + "work_calendars", + self.props, + "active_work_calendar_index", + ) + + if self.props.active_work_calendar_id: + self.draw_editable_ui(context) + + def draw_editable_ui(self, context): + for attribute in self.props.work_calendar_attributes: + row = self.layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + + +class BIM_UL_work_calendars(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=item.name) + if context.scene.BIMWorkCalendarProperties.active_work_calendar_id == item.ifc_definition_id: + row.operator("bim.edit_work_calendar", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_work_calendar", text="", icon="X") + elif context.scene.BIMWorkCalendarProperties.active_work_calendar_id: + row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = item.ifc_definition_id + else: + op = row.operator("bim.enable_editing_work_calendar", text="", icon="GREASEPENCIL") + op.work_calendar = item.ifc_definition_id + row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = item.ifc_definition_id class BIM_PT_tasks(Panel): diff --git a/src/blenderbim/blenderbim/bim/module/void/ui.py b/src/blenderbim/blenderbim/bim/module/void/ui.py index 548ff2f096..20ba1fdb99 100644 --- a/src/blenderbim/blenderbim/bim/module/void/ui.py +++ b/src/blenderbim/blenderbim/bim/module/void/ui.py @@ -27,6 +27,8 @@ class BIM_PT_voids(Panel): for obj in context.selected_objects: if "IfcOpeningElement" in obj.name or not obj.BIMObjectProperties.ifc_definition_id: op.opening = obj.name + elif len(obj.children) == 1 and not obj.children[0].BIMObjectProperties.ifc_definition_id: + op.opening = obj.children[0].name else: op.obj = obj.name diff --git a/src/blenderbim/dxf2ifc.py b/src/blenderbim/dxf2ifc.py index ed0726ab5f..223e5b8fff 100644 --- a/src/blenderbim/dxf2ifc.py +++ b/src/blenderbim/dxf2ifc.py @@ -1,3 +1,6 @@ +import ifcopenshell +import ezdxf + class Dxf2Ifc: def execute(self): self.create_ifc_file() @@ -14,7 +17,7 @@ class Dxf2Ifc: [ self.file.createIfcFaceOuterBound( self.file.createIfcPolyLoop( - [self.file.createIfcCartesianPoint((v.dxf.location)) for v in face[0:3]] + [self.file.createIfcCartesianPoint((face[index].dxf.location)) for index in range(len(face) -1)] ), True, ) diff --git a/src/ifcbimtester/bimtester/features/steps/geolocation/en.py b/src/ifcbimtester/bimtester/features/steps/geolocation/en.py index c83813456c..cd7704bca8 100644 --- a/src/ifcbimtester/bimtester/features/steps/geolocation/en.py +++ b/src/ifcbimtester/bimtester/features/steps/geolocation/en.py @@ -1,6 +1,8 @@ import math +import numpy as np import ifcopenshell import ifcopenshell.util.element +import ifcopenshell.util.placement import ifcopenshell.util.geolocation from behave import step from bimtester import util @@ -171,7 +173,7 @@ def step_impl(context, number): return check_ifc2x3_geolocation("EPset_MapConversion", "Height", number) abscissa = check_ifc4_geolocation("IfcMapConversion", "XAxisAbscissa", should_assert=False) ordinate = check_ifc4_geolocation("IfcMapConversion", "XAxisOrdinate", should_assert=False) - actual_value = round(ifcopenshell.util.geolocation.xy2angle(abscissa, ordinate), 3) + actual_value = round(ifcopenshell.util.geolocation.xaxis2angle(abscissa, ordinate), 3) value = round(number, 3) assert actual_value == value, _('We expected a value of "{}" but instead got "{}"').format(value, actual_value) @@ -186,6 +188,26 @@ def step_impl(context, number): check_ifc4_geolocation("IfcMapConversion", "Scale", number) +@step(u'The model must be rotated clockwise by "{number}" for true north to point up') +def step_impl(context, number): + number = util.assert_number(number) + project = IfcStore.file.by_type("IfcProject")[0] + for c in project.RepresentationContexts: + if c.TrueNorth: + actual_value = round( + ifcopenshell.util.geolocation.yaxis2angle( + c.TrueNorth.DirectionRatios[0], c.TrueNorth.DirectionRatios[1] + ), + 3, + ) + value = round(number, 3) + assert actual_value == value, _('We expected a value of "{}" but instead got "{}"').format( + value, actual_value + ) + return + assert False, _("True north is not defined in the file") + + @step(u'The site "{guid}" has a longitude of "{number}"') def step_impl(context, guid, number): number = util.assert_number(number) @@ -212,3 +234,14 @@ def step_impl(context, guid, number): site = util.assert_guid(IfcStore.file, guid) util.assert_type(site, "IfcSite") util.assert_attribute(site, "RefElevation", number) + + +@step(u'The site "{guid}" must be coincident with the project origin') +def step_impl(context, guid): + site = util.assert_guid(IfcStore.file, guid) + util.assert_type(site, "IfcSite") + if not site.ObjectPlacement: + assert False, _("The site has no object placement") + site_placement = ifcopenshell.util.placement.get_local_placement(site.ObjectPlacement)[:,3][0:3] + origin = np.array([0, 0, 0]) + assert np.allclose(origin, site_placement), _('The site location is at "{}" instead of "{}"') diff --git a/src/ifcgeom/IfcGeom.h b/src/ifcgeom/IfcGeom.h index 32e9701138..b3731ff072 100644 --- a/src/ifcgeom/IfcGeom.h +++ b/src/ifcgeom/IfcGeom.h @@ -507,10 +507,15 @@ public: virtual bool convert_placement(IfcUtil::IfcBaseClass* item, gp_Trsf& trsf) { if (item->as()) { - return convert(item->as(), trsf); - } else { - return false; + try { + return convert(item->as(), trsf); + } catch (std::exception& e) { + Logger::Error(e, item); + } catch (...) { + Logger::Error("Failed processing placement", item); + } } + return false; } }; diff --git a/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py b/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py index 805de997cc..379ee8fdce 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py +++ b/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py @@ -36,9 +36,9 @@ class Usecase: self.file.remove(decomposes) if is_decomposed_by: - related_objects = list(is_decomposed_by.RelatedObjects) - related_objects.append(self.settings["product"]) - is_decomposed_by.RelatedObjects = related_objects + related_objects = set(is_decomposed_by.RelatedObjects) + related_objects.add(self.settings["product"]) + is_decomposed_by.RelatedObjects = list(related_objects) ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": is_decomposed_by}) else: is_decomposed_by = self.file.create_entity( @@ -50,3 +50,4 @@ class Usecase: "RelatingObject": self.settings["relating_object"], } ) + return is_decomposed_by diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py index 63b7d17a2b..9cf6bb06ef 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py @@ -1,6 +1,3 @@ -import ifcopenshell - - class Usecase: def __init__(self, file, **settings): self.file = file diff --git a/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py b/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py index 4c4d310834..fe96c66ded 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py +++ b/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py @@ -21,7 +21,12 @@ class Usecase: context = self.file.createIfcGeometricRepresentationContext(None, "Plan", 2, 1.0e-05, self.origin) else: context = self.file.createIfcGeometricRepresentationContext(None, "Model", 3, 1.0e-05, self.origin) - project = self.file.by_type("IfcProject")[0] + + if self.file.schema == "IFC2X3": + project = self.file.by_type("IfcProject")[0] + else: + project = self.file.by_type("IfcContext")[0] + if project.RepresentationContexts: contexts = list(project.RepresentationContexts) else: diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/data.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/data.py index b8d029b2be..7e1f9ebddb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/data.py @@ -2,12 +2,14 @@ class Data: is_loaded = False map_conversion = {} projected_crs = {} + true_north = {} @classmethod def purge(cls): cls.is_loaded = False cls.map_conversion = {} cls.projected_crs = {} + cls.true_north = None @classmethod def load(cls, file): @@ -15,6 +17,7 @@ class Data: return cls.map_conversion = {} cls.projected_crs = {} + cls.true_north = {} if file.schema == "IFC2X3": return for context in file.by_type("IfcGeometricRepresentationContext", include_subtypes=False): @@ -28,4 +31,9 @@ class Data: if cls.projected_crs["MapUnit"]: cls.projected_crs["MapUnit"] = map_conversion.TargetCRS.MapUnit.get_info() break + for context in file.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if not context.TrueNorth: + continue + cls.true_north = context.TrueNorth.DirectionRatios + break cls.is_loaded = True diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py index aafe61dd83..e4ac84221a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py @@ -7,6 +7,7 @@ class Usecase: self.settings = { "map_conversion": {}, "projected_crs": {}, + "true_north": [], "map_unit": "", } for key, value in settings.items(): @@ -21,6 +22,7 @@ class Usecase: setattr(projected_crs, name, value) self.remove_existing_map_unit(projected_crs) self.set_map_unit(projected_crs) + self.set_true_north() def remove_existing_map_unit(self, projected_crs): if projected_crs.MapUnit and len(self.file.get_inverse(projected_crs.MapUnit)) == 1: @@ -50,3 +52,20 @@ class Usecase: self.settings["map_unit"], self.file.createIfcMeasureWithUnit(value_component, si_unit), ) + + def set_true_north(self): + if self.settings["true_north"] == []: + return + for context in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if context.TrueNorth: + if len(self.file.get_inverse(context.TrueNorth)) != 1: + context.TrueNorth = self.file.create_entity("IfcDirection") + else: + context.TrueNorth = self.file.create_entity("IfcDirection") + direction = context.TrueNorth + if self.settings["true_north"] is None: + context.TrueNorth = self.settings["true_north"] + elif context.CoordinateSpaceDimension == 2: + direction.DirectionRatios = self.settings["true_north"][0:2] + else: + direction.DirectionRatios = self.settings["true_north"][0:2] + [0.0] diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/add_constituent.py b/src/ifcopenshell-python/ifcopenshell/api/material/add_constituent.py index f91dda0980..afa23c3a94 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/add_constituent.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/add_constituent.py @@ -1,6 +1,3 @@ -import ifcopenshell - - class Usecase: def __init__(self, file, **settings): self.file = file diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/add_layer.py b/src/ifcopenshell-python/ifcopenshell/api/material/add_layer.py index b5cb426eb9..b895901227 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/add_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/add_layer.py @@ -1,6 +1,3 @@ -import ifcopenshell - - class Usecase: def __init__(self, file, **settings): self.file = file diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py index f833922d30..5ab716644b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py @@ -4,6 +4,7 @@ class Usecase(): self.settings = { "profile": None, "attributes": {}, + "profile_attributes": {}, "material": None } for key, value in settings.items(): @@ -13,3 +14,5 @@ class Usecase(): for name, value in self.settings["attributes"].items(): setattr(self.settings["profile"], name, value) self.settings["profile"].Material = self.settings["material"] + for name, value in self.settings["profile_attributes"].items(): + setattr(self.settings["profile"].Profile, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py index 9c66ae6afc..a149b63531 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py @@ -1,6 +1,3 @@ -import ifcopenshell - - class Usecase: def __init__(self, file, **settings): self.file = file diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py index d953c56a25..88bd925292 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py @@ -1,6 +1,3 @@ -import ifcopenshell - - class Usecase: def __init__(self, file, **settings): self.file = file diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/add_organisation.py b/src/ifcopenshell-python/ifcopenshell/api/owner/add_organisation.py index 6635fdcb8a..8e5583f0c3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/add_organisation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/add_organisation.py @@ -9,4 +9,7 @@ class Usecase: self.settings[key] = value def execute(self): - return self.file.create_entity("IfcOrganization", **self.settings) + if self.file.schema == "IFC2X3": + self.settings["Id"] = self.settings["Identification"] + del self.settings["Identification"] + return self.file.create_entity("IfcOrganization", **self.settings) \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/add_person.py b/src/ifcopenshell-python/ifcopenshell/api/owner/add_person.py index f136af8d16..d19e98adfe 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/add_person.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/add_person.py @@ -10,4 +10,7 @@ class Usecase: self.settings[key] = value def execute(self): - return self.file.create_entity("IfcPerson", **self.settings) + if self.file.schema == "IFC2X3": + self.settings["Id"] = self.settings["Identification"] + del self.settings["Identification"] + return self.file.create_entity("IfcPerson", **self.settings) \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py index 37062d3845..48b5ef8eec 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py @@ -27,7 +27,7 @@ class Usecase: pset = self.file.create_entity( "IfcPropertySet", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.settings["Name"]} ) - has_property_sets = list(self.settings["product"].HasPropertySets) + has_property_sets = list(self.settings["product"].HasPropertySets or []) has_property_sets.append(pset) self.settings["product"].HasPropertySets = has_property_sets elif self.settings["product"].is_a("IfcMaterialDefinition"): diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py index 80c9e16004..1a1fcd64a3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py @@ -18,9 +18,9 @@ class Usecase: elif self.settings["product"].is_a("IfcTypeProduct"): representations = [rm.MappedRepresentation for rm in self.settings["product"].RepresentationMaps or []] for representation in representations: - ifcopenshell.api.run("owner.unassign_representation", + ifcopenshell.api.run("geometry.unassign_representation", self.file, **{"product": self.settings["product"], "representation": representation} ) - ifcopenshell.api.run("owner.remove_representation", self.file, **{"representation": representation}) + ifcopenshell.api.run("geometry.remove_representation", self.file, **{"representation": representation}) # TODO: remove object placement and other relationships self.file.remove(self.settings["product"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_plan.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_plan.py index f885077333..3d8e2cc3ca 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_plan.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_plan.py @@ -25,18 +25,7 @@ class Usecase: work_plan.StartTime = ifcopenshell.util.date.datetime2ifc(self.settings["start_time"], "IfcDateTime") context = self.file.by_type("IfcContext")[0] - if context.Declares: - rel_declares = context.Declares[0] - ifcopenshell.api.run("owner.update_owner_history", self.file, element=rel_declares) - else: - rel_declares = self.file.create_entity("IfcRelDeclares", **{ - "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatingContext": context - }) - - related_definitions = list(rel_declares.RelatedDefinitions or []) - related_definitions.append(work_plan) - rel_declares.RelatedDefinitions = related_definitions - + ifcopenshell.api.run( + "project.assign_declaration", self.file, definition=work_plan, relating_context=context + ) return work_plan diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py index 894a942a40..501af373c4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py @@ -4,19 +4,32 @@ import ifcopenshell.util.date class Data: is_loaded = False work_plans = {} + work_schedules = {} tasks = {} @classmethod def purge(cls): cls.is_loaded = False cls.work_plans = {} + cls.work_schedules = {} + cls.work_calendars = {} cls.tasks = {} @classmethod def load(cls, file): + cls._file = file + if not cls._file: + return + cls.load_work_plans() + cls.load_work_schedules() + cls.load_work_calendars() + cls.load_tasks() + cls.is_loaded = True + + @classmethod + def load_work_plans(cls): cls.work_plans = {} - cls.tasks = {} - for work_plan in file.by_type("IfcWorkPlan"): + for work_plan in cls._file.by_type("IfcWorkPlan"): data = work_plan.get_info() del data["OwnerHistory"] if data["Creators"]: @@ -26,6 +39,32 @@ class Data: if data["FinishTime"]: data["FinishTime"] = ifcopenshell.util.date.ifc2datetime(data["FinishTime"]) cls.work_plans[work_plan.id()] = data - for task in file.by_type("IfcTask"): + + @classmethod + def load_work_schedules(cls): + cls.work_schedules = {} + for work_schedule in cls._file.by_type("IfcWorkSchedule"): + data = work_schedule.get_info() + del data["OwnerHistory"] + if data["Creators"]: + data["Creators"] = [p.id() for p in data["Creators"]] + data["CreationDate"] = ifcopenshell.util.date.ifc2datetime(data["CreationDate"]) + data["StartTime"] = ifcopenshell.util.date.ifc2datetime(data["StartTime"]) + if data["FinishTime"]: + data["FinishTime"] = ifcopenshell.util.date.ifc2datetime(data["FinishTime"]) + cls.work_schedules[work_schedule.id()] = data + + @classmethod + def load_work_calendars(cls): + for work_calendar in cls._file.by_type("IfcWorkCalendar"): + data = work_calendar.get_info() + del data["OwnerHistory"] + del data["WorkingTimes"] + del data["ExceptionTimes"] + cls.work_calendars[work_calendar.id()] = data + + @classmethod + def load_tasks(cls): + cls.tasks = {} + for task in cls._file.by_type("IfcTask"): cls.tasks[task.id()] = {"Name": task.Name, "Identification": task.Identification or ""} - cls.is_loaded=True diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py index 941000f7ff..ddd36ad88e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py @@ -7,8 +7,10 @@ class Usecase: def execute(self): # TODO: do a deep purge - if self.settings["work_plan"].HasContext: - rel_declares = self.settings["work_plan"].HasContext[0] - if len(rel_declares.RelatedDefinitions) == 1: - self.file.remove(rel_declares) + ifcopenshell.api.run( + "project.unassign_declaration", + self.file, + definition=self.settings["work_plan"], + relating_context=self.file.by_type("IfcContext")[0], + ) self.file.remove(self.settings["work_plan"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py index 0312748c45..fe46877cae 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py @@ -1,5 +1,6 @@ import ifcopenshell import ifcopenshell.api +import ifcopenshell.util.placement class Usecase: @@ -43,3 +44,11 @@ class Usecase: "RelatingStructure": self.settings["relating_structure"], } ) + + if getattr(self.settings["product"], "ObjectPlacement", None): + ifcopenshell.api.run( + "geometry.edit_object_placement", + self.file, + product=self.settings["product"], + matrix=ifcopenshell.util.placement.get_local_placement(self.settings["product"].ObjectPlacement), + ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py index f99e41220b..bedebcb3eb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py @@ -33,7 +33,10 @@ class Usecase(): # TODO: handle unit rewriting, which is complicated else: unit_assignment = self.file.createIfcUnitAssignment([u["ifc"] for u in self.settings.values()]) - self.file.by_type("IfcProject")[0].UnitsInContext = unit_assignment + if self.file.schema == "IFC2X3": + self.file.by_type("IfcProject")[0].UnitsInContext = unit_assignment + else: + self.file.by_type("IfcContext")[0].UnitsInContext = unit_assignment return unit_assignment def create_metric_unit(self, unit_type, data): diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 372af71eea..542793a664 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -64,7 +64,11 @@ class facet(metaclass=meta_facet): yield k, getattr(self, k) def __str__(self): - return self.message % dict(list(self)) + di = dict(list(self)) + for k, v in di.items(): + if isinstance(v, str) and not len(v): + di[k] = "not specified" + return self.message % di class entity(facet): @@ -88,19 +92,19 @@ class classification(facet): """ parameters = ["system", "value"] - message = "a classification reference to '%(value)s' from '%(system)s'" + message = "a classification reference '%(value)s' from '%(system)s'" def __call__(self, inst, logger): refs = [] for association in inst.HasAssociations: if association.is_a("IfcRelAssociatesClassification"): cref = association.RelatingClassification - refs.append((cref.ReferencedSource, cref.Name)) + refs.append((cref.ReferencedSource.Name, cref.ItemReference)) return facet_evaluation( (self.system, self.value) in refs, # @todo - "", + "[classification_eval_todo]", ) @@ -109,17 +113,19 @@ class property(facet): The IDS property facet implenented using `ifcopenshell.util.element` """ - parameters = ["property", "propertyset", "value"] - message = "a property '%(property)s' in '%(propertyset)s' with value '%(value)s'" + parameters = ["name", "propertyset", "value"] + + # import pdb;pdb.set_trace() + message = "a property '%(name)s' in '%(propertyset)s' with value '%(value)s'" def __call__(self, inst, logger): props = ifcopenshell.util.element.get_psets(inst) pset = props.get(self.propertyset) - val = pset.get(self.property) if pset else None + val = pset.get(self.name) if pset else None logger.debug("Testing %s == %s", val, self.value) di = { - "property": self.property, + "name": self.name, "propertyset": self.propertyset, "value": val, } @@ -128,13 +134,38 @@ class property(facet): msg = self.message % di else: if pset: - msg = "a set '%(propertyset)s', but no property '%(property)'" % di + msg = "a set '%(propertyset)s', but no property '%(name)'" % di else: msg = "no set '%(propertyset)s'" % di return facet_evaluation(val == self.value, msg) +class material(facet): + """ + The IDS material facet + """ + parameters = ["name", "value"] + message = "a material '%(name)s with value '%(value)s'" + + def __call__(self, inst, logger): + material_relations = [rel for rel in inst.HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] + names = [] + for rel in material_relations: + if rel.RelatingMaterial.is_a() == "IfcMaterialLayerSetUsage": + layers = rel.RelatingMaterial.ForLayerSet.MaterialLayers + names = [layer.Material.Name for layer in layers] + elif rel.RelatingMaterial.is_a() == "IfcMaterial": + names.append(rel.RelatingMaterial.Name) + + + return facet_evaluation( + 0, + # @todo + "[material_eval_todo]", + ) + + class boolean_logic: """ Boolean conjunction over a collection of functions @@ -166,17 +197,39 @@ class restriction: """ def __init__(self, node): - self.options = [ - n.getAttribute("value") - for n in node.childNodes - if n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("enumeration") - ] + self.restriction_on = node.getAttribute("base") + self.options = [] + self.type = [] + + for n in node.childNodes: + if n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("enumeration"): + self.options.append(n.getAttribute("value")) + self.type = "enumeration" + elif n.nodeType == n.ELEMENT_NODE and (n.tagName.endswith("Inclusive") or n.tagName.endswith("Exclusive")): + self.options.append(n.getAttribute("value")) + self.type = "bounds" + elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("length"): + self.options.append(n.getAttribute("value")) + self.type = "length" + elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"): + self.options.append(n.getAttribute("value")) + self.type = "pattern" + + # "Given an instance with %(applicabiliy)s\nWe expect %(requirements)s" % self.__dict__ def __eq__(self, other): return other in self.options def __repr__(self): - return " or ".join(self.options) + if self.type == "enumeration": + return " or ".join(self.options) + elif self.type == "bounds": + self.options.sort() + return "of type %s, having a value between %s and %s" % (self.restriction_on, self.options[0], self.options[1]) + elif self.type == "length": + return "of type %s with a length of %s" % (self.restriction_on, self.options[0]) + elif self.type == "pattern": + return "of type %s respecting pattern %s" % (self.restriction_on, self.options[0]) class specification: @@ -202,10 +255,11 @@ class specification: def __call__(self, inst, logger): if self.applicabiliy(inst, logger): valid = self.requirements(inst, logger) + if valid: - logger.info(str(self) + "\n%s has" % inst + " " + str(valid) + " so is compliant") + logger.info({'guid':inst.GlobalId, 'result':valid.success,'sentence':str(self) + "\n%s has" % inst + " " + str(valid) + " so is compliant"}) else: - logger.error(str(self) + "\n%s has" % inst + " " + str(valid) + " so is not compliant") + logger.error({'guid':inst.GlobalId, 'result':valid.success, 'sentence':str(self) + "\n%s has" % inst + " " + str(valid) + " so is not compliant"}) def __str__(self): return "Given an instance with %(applicabiliy)s\nWe expect %(requirements)s" % self.__dict__ @@ -229,15 +283,17 @@ class ids: for spec in self.specifications: for elem in ifc_file.by_type("IfcObject"): spec(elem, logger) - - + if __name__ == "__main__": - import sys + import sys, os import logging import ifcopenshell + filename = os.path.join(os.getcwd(), "ids.txt") + logger = logging.getLogger("IDS") - logging.basicConfig(level=logging.INFO, format="%(message)s") + logging.basicConfig(filename=filename, level=logging.INFO, format="%(message)s") + logging.FileHandler(filename, mode='w') ids_file = ids(sys.argv[1]) ifc_file = ifcopenshell.open(sys.argv[2]) diff --git a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py index a3f5d1722d..b3f3b568c6 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py @@ -94,6 +94,16 @@ def global2local(matrix, eastings, northings, orthogonal_height, x_axis_abscissa ) -# Used for converting the X and Y vectors of the X Axis in IFC geolocation -def xy2angle(x, y): +# Used for converting the X and Y vectors of the X Axis in IFC grid north geolocation +def xaxis2angle(x, y): return math.degrees(math.atan2(y, x)) + + +# Used for converting the X and Y vectors of the Y Axis in IFC true north geolocation +def yaxis2angle(x, y): + angle = math.degrees(math.atan2(y, x)) - 90 + if angle < -180: + angle += 360 + elif angle > 180: + angle -= 360 + return angle diff --git a/src/serializers/SvgSerializer.cpp b/src/serializers/SvgSerializer.cpp index 7650ac46c9..3b3d8ae8fd 100644 --- a/src/serializers/SvgSerializer.cpp +++ b/src/serializers/SvgSerializer.cpp @@ -181,6 +181,7 @@ void SvgSerializer::write(path_object& p, const TopoDS_Wire& wire, boost::option path.add(center.Y()); // @todo isn't there a ")" missing here? // @todo also X, Y are not added to {x,y}coords vector + // @todo also z_rotation is in radians, should be in degrees // Bounding box: // More important to have all geometry in bounding box than to be minimal @@ -1060,6 +1061,11 @@ void SvgSerializer::write(const geometry_data& data) { z_rotation -= 180; } + std::string text_offset = "8"; + if (scale_) { + text_offset = "1"; + } + util::string_buffer path; // dominant-baseline="central" is not well supported in IE. // so we add a 0.35 offset to the dy of the tspans @@ -1073,7 +1079,7 @@ void SvgSerializer::write(const geometry_data& data) { xcoords.push_back(path.add(center.X())); path.add(" "); ycoords.push_back(path.add(center.Y())); - path.add(") translate(0 -8)\">"); + path.add(") translate(0 -" + text_offset + ")\">"); std::vector labels{}; @@ -1302,15 +1308,28 @@ void SvgSerializer::write(const geometry_data& data) { 10 * ((pa.X() - pb.X()) * (pa.X() - pb.X())) + 1 * ((pa.Y() - pb.Y()) * (pa.Y() - pb.Y())) ); - if (d > furthest_points_distance) { - gp_Pnt p3d((pa.XYZ() + pb.XYZ()) / 2.); - gp_Pnt2d p2d(p3d.X(), p3d.Y()); - if (fcls.Perform(p2d) == TopAbs_IN) { + if (d > furthest_points_distance) { + + // Sample some points on the line and assure it's inside. + bool all_inside = true; + for (int i = 5; i < 95; ++i) { + gp_Pnt p3d((pa.XYZ() + (pb.XYZ() - pa.XYZ()) * i / 100.)); + gp_Pnt2d p2d(p3d.X(), p3d.Y()); + + if (fcls.Perform(p2d) != TopAbs_IN) { + all_inside = false; + } + } + + if (all_inside) { + gp_Pnt p3d((pa.XYZ() + pb.XYZ()) * 0.5); + gp_Pnt2d p2d(p3d.X(), p3d.Y()); furthest_points = { &pa, &pb }; furthest_points_distance = d; center_point = p3d; } + } } } @@ -1563,7 +1582,136 @@ void SvgSerializer::resetScale() { ymax = -std::numeric_limits::infinity(); } +void SvgSerializer::addTextAnnotations(const drawing_key& k) { + auto& meta = drawing_metadata[k]; + + boost::optional> range; + + if (k.first && section_data_) { + for (auto& sd : *section_data_) { + if (sd.which() == 0) { + const auto& plan = boost::get(sd); + if (k.first == plan.storey) { + range = std::make_pair(plan.elevation, plan.next_elevation); + } + } + } + } + + auto annotations = file->instances_by_type("IfcAnnotation"); + if (annotations) { + for (auto& ann_ : *annotations) { + auto ann = (IfcUtil::IfcBaseEntity*) ann_; + + auto ot = ann->get("ObjectType"); + auto nm = ann->get("Name"); + auto ds = ann->get("Description"); + auto pl = ann->get("ObjectPlacement"); + + if (!ot->isNull() && !nm->isNull() && !ds->isNull() && !pl->isNull()) { + auto object_type = (std::string) *ot; + auto name = (std::string) *nm; + auto desc = (std::string) *ds; + + if (object_type == "Text") { + IfcGeom::Kernel kernel(file); + gp_Trsf trsf; + if (kernel.convert_placement(*pl, trsf)) { + + auto v = trsf.TranslationPart(); + if (k.first) { + v.ChangeCoord(1) *= -1.; + trsf.SetTranslationPart(v); + } + + if (!range || (v.Z() >= range->first && v.Z() < range->second)) { + + if (meta.pln_3d.Position().Direction().Dot(gp_Dir(trsf.HVectorialPart().Column(3))) > 0.99) { + auto svg_name = nameElement(ann); + path_object* po; + if (k.first) { + po = &start_path(meta.pln_3d, k.first, svg_name); + } + else { + po = &start_path(meta.pln_3d, k.second, svg_name); + } + + if (object_type.size()) { + // postfix the object_type for CSS matching + boost::replace_all(svg_name, "class=\"IfcAnnotation\"", "class=\"IfcAnnotation " + object_type + "\""); + } + + boost::optional font_size; + std::vector tokens; + boost::split(tokens, name, boost::is_any_of("_")); + if (tokens.size() == 2) { + try { + font_size = boost::lexical_cast(tokens.back()); + } + catch (...) {} + } + + // @todo column or row? + double z_rotation = gp_Dir(trsf.HVectorialPart().Column(1)).AngleWithRef(gp_Dir(1., 0., 0.), gp_Dir(0., 0., 1.)); + z_rotation *= 180. / M_PI; + + util::string_buffer path; + // dominant-baseline="central" is not well supported in IE. + // so we add a 0.35 offset to the dy of the tspans + path.add(" "); + + std::vector labels{ desc }; + + for (auto lit = labels.begin(); lit != labels.end(); ++lit) { + const auto& l = *lit; + double dy = labels.begin() == lit + ? 0.0 // align bottom + : 1.0; // <- dy is relative to the previous text element, so + // always 1 for successive spans. + path.add("(dy)); + path.add("em\">"); + path.add(l); + path.add(""); + } + + path.add(""); + + po->second.push_back(path); + } + } + } + } + } + } + } +} + void SvgSerializer::finalize() { + for (auto& p : drawing_metadata) { + addTextAnnotations(p.first); + } + for (auto& p : storey_hlr) { draw_hlr(drawing_metadata[{p.first, ""}].pln_3d, { p.first, "" }); } diff --git a/src/serializers/SvgSerializer.h b/src/serializers/SvgSerializer.h index 0772e03bdb..ede299dea3 100644 --- a/src/serializers/SvgSerializer.h +++ b/src/serializers/SvgSerializer.h @@ -220,6 +220,7 @@ public: void setDrawDoorArcs(bool b) { draw_door_arcs_ = b; } void setStoreyHeightLineLength(double d) { storey_height_line_length_ = d; } void setSpaceNameTransform(const std::string& v) { space_name_transform_ = v; } + void addTextAnnotations(const drawing_key& k); std::array, 3> resize(); void resetScale();