From fd8cb7e8edd109a7d9a881c055d3dc49d6d46615 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 22 Jun 2024 23:32:25 +1000 Subject: [PATCH] Implement Blender UI for handling new coordinate operations in IFC4X3 --- .../bim/module/georeference/data.py | 43 ++++++++----- .../bim/module/georeference/operator.py | 2 +- .../bim/module/georeference/prop.py | 12 +++- .../blenderbim/bim/module/georeference/ui.py | 20 +++--- .../blenderbim/core/georeference.py | 12 ++-- src/blenderbim/blenderbim/core/tool.py | 4 +- .../blenderbim/tool/georeference.py | 64 +++++++++++++++---- .../ifcopenshell/util/geolocation.py | 7 +- 8 files changed, 113 insertions(+), 51 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/georeference/data.py b/src/blenderbim/blenderbim/bim/module/georeference/data.py index 3a55d800fb..9efb0f912f 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/data.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/data.py @@ -20,6 +20,7 @@ import bpy import ifcopenshell.util.geolocation import blenderbim.tool as tool +from ifcopenshell.util.doc import get_entity_doc def refresh(): @@ -32,8 +33,9 @@ class GeoreferenceData: @classmethod def load(cls): + cls.data["coordinate_operation_class"] = cls.coordinate_operation_class() cls.data["blender_derived_angle"] = cls.blender_derived_angle() - cls.data["map_conversion"] = cls.map_conversion() + cls.data["coordinate_operation"] = cls.coordinate_operation() cls.data["map_derived_angle"] = cls.map_derived_angle() cls.data["projected_crs"] = cls.projected_crs() cls.data["true_north"] = cls.true_north() @@ -42,6 +44,14 @@ class GeoreferenceData: cls.data["map_unit_symbol"] = cls.map_unit_symbol() cls.is_loaded = True + @classmethod + def coordinate_operation_class(cls): + declaration = tool.Ifc.schema().declaration_by_name("IfcCoordinateOperation") + declarations = ifcopenshell.util.schema.get_subtypes(declaration) + names = [d.name() for d in declarations] + version = tool.Ifc.get_schema() + return [(c, c, get_entity_doc(version, c).get("description", "")) for c in sorted(names)] + @classmethod def blender_derived_angle(cls): props = bpy.context.scene.BIMGeoreferenceProperties @@ -56,36 +66,37 @@ class GeoreferenceData: ) @classmethod - def map_conversion(cls): + def coordinate_operation(cls): if tool.Ifc.get_schema() == "IFC2X3": project = tool.Ifc.get().by_type("IfcProject")[0] - map_conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion") - if not map_conversion: + coordinate_operation = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion") + if not coordinate_operation: return {} - del map_conversion["id"] - return map_conversion + del coordinate_operation["id"] + coordinate_operation["type"] = "ePSet_MapConversion" + return coordinate_operation for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): if context.HasCoordinateOperation: - map_conversion = context.HasCoordinateOperation[0].get_info() - del map_conversion["id"] - del map_conversion["type"] - del map_conversion["SourceCRS"] - del map_conversion["TargetCRS"] - return map_conversion + coordinate_operation = context.HasCoordinateOperation[0].get_info() + del coordinate_operation["id"] + del coordinate_operation["SourceCRS"] + del coordinate_operation["TargetCRS"] + return coordinate_operation return {} @classmethod def map_derived_angle(cls): if ( - cls.data["map_conversion"] - and cls.data["map_conversion"].get("XAxisAbscissa", None) is not None - and cls.data["map_conversion"].get("XAxisOrdinate", None) is not None + cls.data["coordinate_operation"] + and cls.data["coordinate_operation"].get("XAxisAbscissa", None) is not None + and cls.data["coordinate_operation"].get("XAxisOrdinate", None) is not None ): return str( round( ifcopenshell.util.geolocation.xaxis2angle( - cls.data["map_conversion"]["XAxisAbscissa"], cls.data["map_conversion"]["XAxisOrdinate"] + cls.data["coordinate_operation"]["XAxisAbscissa"], + cls.data["coordinate_operation"]["XAxisOrdinate"], ), 3, ) diff --git a/src/blenderbim/blenderbim/bim/module/georeference/operator.py b/src/blenderbim/blenderbim/bim/module/georeference/operator.py index 3024e7c543..8596ec8d0a 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/operator.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/operator.py @@ -29,7 +29,7 @@ class AddGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Add a new georeference" def _execute(self, context): - core.add_georeferencing(tool.Ifc) + core.add_georeferencing(tool.Georeference) class EnableEditingGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/blenderbim/blenderbim/bim/module/georeference/prop.py b/src/blenderbim/blenderbim/bim/module/georeference/prop.py index 9351945094..add74e8ea5 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/prop.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/prop.py @@ -29,11 +29,21 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from blenderbim.bim.module.georeference.data import GeoreferenceData + + +def get_coordinate_operation_class(self, context): + if not GeoreferenceData.is_loaded: + GeoreferenceData.load() + return GeoreferenceData.data["coordinate_operation_class"] class BIMGeoreferenceProperties(PropertyGroup): + coordinate_operation_class: bpy.props.EnumProperty( + items=get_coordinate_operation_class, name="Coordinate Operation Class" + ) is_editing: BoolProperty(name="Is Editing") - map_conversion: CollectionProperty(name="Map Conversion", type=Attribute) + coordinate_operation: CollectionProperty(name="Coordinate Operation", type=Attribute) projected_crs: CollectionProperty(name="Projected CRS", type=Attribute) local_coordinates: StringProperty( name="Local Coordinates", description='Formatted "x,y,z" (without quotes)', default="0,0,0" diff --git a/src/blenderbim/blenderbim/bim/module/georeference/ui.py b/src/blenderbim/blenderbim/bim/module/georeference/ui.py index 2223b55fc4..e81cf9015f 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/ui.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/ui.py @@ -53,9 +53,9 @@ class BIM_PT_gis(Panel): draw_attributes(props.projected_crs, self.layout) row = self.layout.row() - row.label(text="Map Conversion", icon="GRID") + row.label(text="Coordinate Operation", icon="GRID") - for attribute in props.map_conversion: + for attribute in props.coordinate_operation: 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") @@ -80,12 +80,14 @@ class BIM_PT_gis(Panel): if tool.Ifc.get_schema() == "IFC2X3": row = self.layout.row() - row.label(text="IFC2X3 Fallback In Use", icon="ERROR") + row.label(text="IFC2X3 Fallback In Use", icon="INFO") if not GeoreferenceData.data["projected_crs"]: - row = self.layout.row(align=True) - row.label(text="Not Georeferenced") + row = self.layout.row() + row.label(text="Not Georeferenced", icon="ERROR") if tool.Ifc.get_schema() != "IFC2X3": + row = self.layout.row(align=True) + row.prop(props, "coordinate_operation_class", text="") row.operator("bim.add_georeferencing", icon="ADD", text="") if props.has_blender_offset: @@ -125,13 +127,15 @@ class BIM_PT_gis(Panel): row.label(text=key) row.label(text=str(value)) - if GeoreferenceData.data["map_conversion"]: + if GeoreferenceData.data["coordinate_operation"]: row = self.layout.row(align=True) - row.label(text="Map Conversion", icon="GRID") + row.label(text="Coordinate Operation", icon="GRID") - for key, value in GeoreferenceData.data["map_conversion"].items(): + for key, value in GeoreferenceData.data["coordinate_operation"].items(): if value is None: continue + if key == "type": + key = "Type" row = self.layout.row(align=True) if key in ("Eastings", "Northings", "OrthogonalHeight"): row.label(text=f"{key} ({GeoreferenceData.data['map_unit_symbol']})") diff --git a/src/blenderbim/blenderbim/core/georeference.py b/src/blenderbim/blenderbim/core/georeference.py index 66b12b0259..22035b9dda 100644 --- a/src/blenderbim/blenderbim/core/georeference.py +++ b/src/blenderbim/blenderbim/core/georeference.py @@ -17,13 +17,13 @@ # along with BlenderBIM Add-on. If not, see . -def add_georeferencing(ifc): - ifc.run("georeference.add_georeferencing") +def add_georeferencing(georeference): + georeference.add_georeferencing() def enable_editing_georeferencing(georeference): georeference.import_projected_crs() - georeference.import_map_conversion() + georeference.import_coordinate_operation() georeference.import_true_north() georeference.enable_editing() @@ -40,7 +40,7 @@ def edit_georeferencing(ifc, georeference): ifc.run( "georeference.edit_georeferencing", projected_crs=georeference.get_projected_crs_attributes(), - map_conversion=georeference.get_map_conversion_attributes(), + coordinate_operation=georeference.get_coordinate_operation_attributes(), true_north=georeference.get_true_north_attributes(), ) georeference.disable_editing() @@ -77,9 +77,11 @@ def convert_global_to_local(georeference): georeference.set_coordinates("local", coordinates) georeference.set_cursor_location() + def convert_angle_to_coord(georeference, type): vector_coordinates = georeference.angle2coords(georeference.get_angle(type), type) - georeference.set_vector_coordinates(vector_coordinates,type) + georeference.set_vector_coordinates(vector_coordinates, type) + def import_plot(georeference, filepath): georeference.import_plot(filepath) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 1cbe4ef714..8e7f6bef64 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -423,7 +423,7 @@ class Georeference: def get_angle(cls, type): pass def get_coordinates(cls, io): pass def get_cursor_location(cls): pass - def get_map_conversion_attributes(cls): pass + def get_coordinate_operation_attributes(cls): pass def get_projected_crs_attributes(cls): pass def get_true_north_attributes(cls): pass def import_map_conversion(cls): pass @@ -1019,4 +1019,4 @@ class Voider: @interface class Web: - pass \ No newline at end of file + pass diff --git a/src/blenderbim/blenderbim/tool/georeference.py b/src/blenderbim/blenderbim/tool/georeference.py index 459e29c2ab..2578134611 100644 --- a/src/blenderbim/blenderbim/tool/georeference.py +++ b/src/blenderbim/blenderbim/tool/georeference.py @@ -22,10 +22,17 @@ import blenderbim.core.tool import blenderbim.tool as tool import ifcopenshell import blenderbim.bim.helper -from math import radians, degrees, atan, tan, cos, sin +from math import radians, cos, sin class Georeference(blenderbim.core.tool.Georeference): + @classmethod + def add_georeferencing(cls): + tool.Ifc.run( + "georeference.add_georeferencing", + ifc_class=bpy.context.scene.BIMGeoreferenceProperties.coordinate_operation_class, + ) + @classmethod def import_projected_crs(cls): def callback(name, prop, data): @@ -59,24 +66,44 @@ class Georeference(blenderbim.core.tool.Georeference): return @classmethod - def import_map_conversion(cls): + def import_coordinate_operation(cls): def callback(name, prop, data): - if name not in ["SourceCRS", "TargetCRS"]: + if name in ("FirstCoordinate", "SecondCoordinate"): + props = bpy.context.scene.BIMGeoreferenceProperties + if name == "FirstCoordinate": + new = props.coordinate_operation.add() + new.name = "Measure Type" + new.data_type = "enum" + new.is_optional = False + new.is_null = False + new.enum_items = json.dumps(["IfcLengthMeasure", "IfcPlaneAngleMeasure"]) + new.enum_value = data[name].is_a() + prop = props.coordinate_operation.add() + prop.name = name + prop.is_optional = False + prop.is_null = False + # Enforce a string data type to prevent data loss in single-precision Blender props + prop.data_type = "string" + prop.string_value = "" if prop.is_null else str(data[name].wrappedValue) + return True + elif name not in ("SourceCRS", "TargetCRS"): # Enforce a string data type to prevent data loss in single-precision Blender props prop.data_type = "string" prop.string_value = "" if prop.is_null else str(data[name]) return True props = bpy.context.scene.BIMGeoreferenceProperties - props.map_conversion.clear() + props.coordinate_operation.clear() if tool.Ifc.get_schema() == "IFC2X3": return for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): if context.HasCoordinateOperation: - map_conversion = context.HasCoordinateOperation[0] - blenderbim.bim.helper.import_attributes2(map_conversion, props.map_conversion, callback=callback) + coordinate_operation = context.HasCoordinateOperation[0] + blenderbim.bim.helper.import_attributes2( + coordinate_operation, props.coordinate_operation, callback=callback + ) return @classmethod @@ -105,15 +132,24 @@ class Georeference(blenderbim.core.tool.Georeference): return blenderbim.bim.helper.export_attributes(props.projected_crs, callback=callback) @classmethod - def get_map_conversion_attributes(cls): + def get_coordinate_operation_attributes(cls): + measure_type = None + def callback(attributes, prop): - if not prop.is_null and prop.data_type == "string": + global measure_type + if prop.name == "Measure Type": + measure_type = prop.get_value() + return True + elif prop.name in ("FirstCoordinate", "SecondCoordinate"): + attributes[prop.name] = tool.Ifc.get().create_entity(measure_type, float(prop.string_value)) + return True + elif not prop.is_null and prop.data_type == "string": # We store our floats as string to prevent single precision data loss attributes[prop.name] = float(prop.string_value) return True props = bpy.context.scene.BIMGeoreferenceProperties - return blenderbim.bim.helper.export_attributes(props.map_conversion, callback=callback) + return blenderbim.bim.helper.export_attributes(props.coordinate_operation, callback=callback) @classmethod def get_true_north_attributes(cls): @@ -210,15 +246,15 @@ class Georeference(blenderbim.core.tool.Georeference): def set_ifc_grid_north(cls): x_angle = bpy.context.scene.sun_pos_properties.north_offset props = bpy.context.scene.BIMGeoreferenceProperties - props.map_conversion.get("XAxisAbscissa").string_value = str(cos(x_angle)) - props.map_conversion.get("XAxisOrdinate").string_value = str(sin(x_angle)) + props.coordinate_operation.get("XAxisAbscissa").string_value = str(cos(x_angle)) + props.coordinate_operation.get("XAxisOrdinate").string_value = str(sin(x_angle)) @classmethod def set_blender_grid_north(cls): props = bpy.context.scene.BIMGeoreferenceProperties angle = ifcopenshell.util.geolocation.xaxis2angle( - float(props.map_conversion.get("XAxisAbscissa").string_value), - float(props.map_conversion.get("XAxisOrdinate").string_value), + float(props.coordinate_operation.get("XAxisAbscissa").string_value), + float(props.coordinate_operation.get("XAxisOrdinate").string_value), ) bpy.context.scene.sun_pos_properties.north_offset = -radians(angle) @@ -247,7 +283,7 @@ class Georeference(blenderbim.core.tool.Georeference): bpy.context.scene.BIMGeoreferenceProperties.y_axis_ordinate_output = str(y) @classmethod - def import_plot(cls, filepath, map_conversion): + def import_plot(cls, filepath): import bmesh def parse_csv(file_path): diff --git a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py index c97558cd24..876023fc2e 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py @@ -226,13 +226,12 @@ def get_helmert_transformation_parameters(ifc_file: ifcopenshell.file) -> Option else: factor_x = factor_y = factor_z = 1 elif conversion.is_a() == "IfcRigidOperation": - # TODO - e = conversion.FirstCoordinate - n = conversion.SecondCoordinate + e = conversion.FirstCoordinate.wrappedValue + n = conversion.SecondCoordinate.wrappedValue h = conversion.Height or 0 xaa = 1.0 xao = 0.0 - factor_x = factor_y = factor_z = 1 + scale = factor_x = factor_y = factor_z = 1 if not xaa and not xao: xaa = 1.0