diff --git a/src/blenderbim/blenderbim/bim/module/georeference/data.py b/src/blenderbim/blenderbim/bim/module/georeference/data.py index 93366b6baa..66df12b93b 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/data.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/data.py @@ -16,77 +16,108 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . + +import bpy +import ifcopenshell.util.geolocation import blenderbim.tool as tool + def refresh(): GeoreferenceData.is_loaded = False - + + class GeoreferenceData: data = {} is_loaded = False - + @classmethod - def load(cls): - cls.data = { - "map_conversion" : cls.map_conversion(), - "projected_crs" : cls.projected_crs(), - "true_north" : cls.true_north(), - } + def load(cls): + cls.data["blender_derived_angle"] = cls.blender_derived_angle() + cls.data["map_conversion"] = cls.map_conversion() + cls.data["map_derived_angle"] = cls.map_derived_angle() + cls.data["projected_crs"] = cls.projected_crs() + cls.data["true_north"] = cls.true_north() + cls.data["true_derived_angle"] = cls.true_derived_angle() cls.is_loaded = True - + + @classmethod + def blender_derived_angle(cls): + props = bpy.context.scene.BIMGeoreferenceProperties + if props.has_blender_offset: + return str( + round( + ifcopenshell.util.geolocation.xaxis2angle( + float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate) + ), + 3, + ) + ) + @classmethod def map_conversion(cls): - ifc = tool.Ifc.get() - map_conversion_v = {} - - if ifc.schema == "IFC2X3": - return - - for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): - if not context.HasCoordinateOperation: - continue - - map_conversion_v = context.HasCoordinateOperation[0].get_info() - map_conversion_v["SourceCRS"] = map_conversion_v["SourceCRS"].id() - map_conversion_v["TargetCRS"] = map_conversion_v["TargetCRS"].id() - - break - - return map_conversion_v - + 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].get_info() + del map_conversion["id"] + del map_conversion["type"] + del map_conversion["SourceCRS"] + del map_conversion["TargetCRS"] + return map_conversion + return {} + + @classmethod + def map_derived_angle(cls): + if ( + cls.data["map_conversion"] + and cls.data["map_conversion"]["XAxisAbscissa"] is not None + and cls.data["map_conversion"]["XAxisOrdinate"] is not None + ): + return str( + round( + ifcopenshell.util.geolocation.xaxis2angle( + cls.data["map_conversion"]["XAxisAbscissa"], cls.data["map_conversion"]["XAxisOrdinate"] + ), + 3, + ) + ) + return "" + @classmethod def projected_crs(cls): - ifc = tool.Ifc.get() - projected_crs = {} - - if ifc.schema == "IFC2X3": - return - - for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): - if not context.HasCoordinateOperation: - continue - - projected_crs = context.HasCoordinateOperation[0].TargetCRS.get_info() - if projected_crs["MapUnit"]: - projected_crs["MapUnit"] = map_conversion.TargetCRS.MapUnit.get_info() + if tool.Ifc.get_schema() == "IFC2X3": + return {} + + for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if context.HasCoordinateOperation: + projected_crs = context.HasCoordinateOperation[0].TargetCRS.get_info() + del projected_crs["id"] + del projected_crs["type"] + if projected_crs["MapUnit"]: + unit = projected_crs["MapUnit"] + projected_crs["MapUnit"] = (getattr(unit, "Prefix", "") or "") + unit.Name + return projected_crs + return {} - break - - return projected_crs - @classmethod def true_north(cls): ifc = tool.Ifc.get() true_north = {} - + if ifc.schema == "IFC2X3": return - + for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): if not context.TrueNorth: continue true_north = context.TrueNorth.DirectionRatios break - + return true_north + @classmethod + def true_derived_angle(cls): + if cls.data["true_north"]: + return str(round(ifcopenshell.util.geolocation.yaxis2angle(*cls.data["true_north"][0:2]), 3)) diff --git a/src/blenderbim/blenderbim/bim/module/georeference/operator.py b/src/blenderbim/blenderbim/bim/module/georeference/operator.py index af7d34dec5..71bde91ba8 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/operator.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/operator.py @@ -21,6 +21,7 @@ import bpy import blenderbim.tool as tool import blenderbim.core.georeference as core + class AddGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_georeferencing" bl_label = "Add Georeferencing" @@ -29,34 +30,38 @@ class AddGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.add_georeferencing(tool.Ifc) - + + class EnableEditingGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.enable_editing_georeferencing" bl_label = "Enable Editing Georeferencing" bl_options = {"REGISTER", "UNDO"} bl_description = "Enable editing georeferencing" - + def _execute(self, context): core.enable_editing_georeferencing(tool.Georeference) + class RemoveGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_georeferencing" bl_label = "Remove Georeferencing" bl_options = {"REGISTER", "UNDO"} bl_description = "Remove the georeferencing" - + def _execute(self, context): core.remove_georeferencing(tool.Ifc) - + + class EditGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.edit_georeferencing" bl_label = "Edit Georeferencing" bl_options = {"REGISTER", "UNDO"} bl_description = "Edit the georeferencing" - + def _execute(self, context): core.edit_georeferencing(tool.Ifc, tool.Georeference) - + + class DisableEditingGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.disable_editing_georeferencing" bl_label = "Disable Editing Georeferencing" @@ -66,37 +71,41 @@ class DisableEditingGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.disable_editing_georeferencing(tool.Georeference) + class SetIfcGridNorth(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.set_ifc_grid_north" bl_label = "Set IFC Grid North" bl_options = {"REGISTER", "UNDO"} bl_description = "Set IFC grid north" - + def _execute(self, context): core.set_ifc_grid_north(tool.Georeference) + class SetBlenderGridNorth(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.set_blender_grid_north" bl_label = "Set Blender Grid North" bl_options = {"REGISTER", "UNDO"} bl_description = "Set Blender grid north" - + def _execute(self, context): core.set_blender_grid_north(tool.Georeference) + class GetCursorLocation(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.get_cursor_location" bl_label = "Get Cursor Location" bl_options = {"REGISTER", "UNDO"} bl_description = "Insert the current cursor coordinates" - + @classmethod def poll(cls, context): return tool.Ifc.get() - + def _execute(self, context): core.get_cursor_location(tool.Georeference) - + + class SetCursorLocation(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.set_cursor_location" bl_label = "Set Cursor Location" @@ -105,13 +114,13 @@ class SetCursorLocation(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - file = tool.Ifc.get() props = context.scene.BIMGeoreferenceProperties - return file and file.by_type("IfcUnitAssignment") and props.coordinate_output.count(",") == 2 - + return tool.Ifc.get() and props.coordinate_output.count(",") == 2 + def _execute(self, context): core.set_cursor_location(tool.Georeference) - + + class SetIfcTrueNorth(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.set_ifc_true_north" bl_label = "Set IFC True North" @@ -121,6 +130,7 @@ class SetIfcTrueNorth(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.set_ifc_true_north(tool.Georeference) + class SetBlenderTrueNorth(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.set_blender_true_north" bl_label = "Set Blender True North" @@ -130,33 +140,34 @@ class SetBlenderTrueNorth(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.set_blender_true_north(tool.Georeference) + class ConvertLocalToGlobal(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.convert_local_to_global" bl_label = "Convert Local To Global" bl_options = {"REGISTER", "UNDO"} bl_description = "Convert local coordinate to global coordinate" - + @classmethod def poll(cls, context): file = tool.Ifc.get() props = context.scene.BIMGeoreferenceProperties return file and props.coordinate_input.count(",") == 2 - + def _execute(self, context): core.convert_local_to_global(tool.Georeference) - + + class ConvertGlobalToLocal(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.convert_global_to_local" bl_label = "Convert Global To Local" bl_options = {"REGISTER", "UNDO"} bl_description = "Convert global coordinate to local coordinate" - + @classmethod def poll(cls, context): file = tool.Ifc.get() props = context.scene.BIMGeoreferenceProperties return file and file.by_type("IfcUnitAssignment") and props.coordinate_input.count(",") == 2 - + def _execute(self, context): core.convert_global_to_local(tool.Georeference) - diff --git a/src/blenderbim/blenderbim/bim/module/georeference/ui.py b/src/blenderbim/blenderbim/bim/module/georeference/ui.py index 93dcc34c38..15c28e3467 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/ui.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/ui.py @@ -16,12 +16,12 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . -import ifcopenshell.util.geolocation +import blenderbim.tool as tool from bpy.types import Panel -from blenderbim.bim.ifc import IfcStore from blenderbim.bim.helper import draw_attributes, draw_attribute from blenderbim.bim.module.georeference.data import GeoreferenceData + class BIM_PT_gis(Panel): bl_label = "IFC Georeferencing" bl_idname = "BIM_PT_gis" @@ -30,15 +30,15 @@ class BIM_PT_gis(Panel): bl_region_type = "WINDOW" bl_context = "scene" bl_parent_id = "BIM_PT_geometry" - + def draw(self, context): self.layout.use_property_split = True self.layout.use_property_decorate = False props = context.scene.BIMGeoreferenceProperties - + if not GeoreferenceData.is_loaded: GeoreferenceData.load() - + if props.is_editing: return self.draw_editable_ui(context) self.draw_ui(context) @@ -75,16 +75,15 @@ class BIM_PT_gis(Panel): 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 GeoreferenceData.data["projected_crs"]: row = self.layout.row(align=True) row.label(text="Not Georeferenced") - if IfcStore.get_file().schema != "IFC2X3": #TODO Is it correct to use IfcStore.get_file() or is it better tool.Ifc.get()? + if tool.Ifc.get_schema != "IFC2X3": row.operator("bim.add_georeferencing", icon="ADD", text="") - + if props.has_blender_offset: row = self.layout.row() row.label(text="Blender Offset", icon="TRACKING_REFINE_FORWARDS") @@ -106,16 +105,7 @@ class BIM_PT_gis(Panel): row.label(text=props.blender_x_axis_ordinate) row = self.layout.row(align=True) 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, - ) - ) - ) + row.label(text=GeoreferenceData.data["blender_derived_angle"]) if GeoreferenceData.data["projected_crs"]: row = self.layout.row(align=True) @@ -124,12 +114,8 @@ class BIM_PT_gis(Panel): row.operator("bim.remove_georeferencing", icon="X", text="") for key, value in GeoreferenceData.data["projected_crs"].items(): - if key == "id" or key == "type" or not value: + if not value: continue - if key == "MapUnit": - unit_value = value.get("Prefix", "") or "" - unit_value += value["Name"] - value = unit_value row = self.layout.row(align=True) row.label(text=key) row.label(text=str(value)) @@ -139,7 +125,7 @@ class BIM_PT_gis(Panel): row.label(text="Map Conversion", icon="GRID") for key, value in GeoreferenceData.data["map_conversion"].items(): - if key == "id" or key == "type" or key == "SourceCRS" or key == "TargetCRS" or value is None: + if value is None: continue row = self.layout.row(align=True) row.label(text=key) @@ -147,16 +133,7 @@ class BIM_PT_gis(Panel): if key == "XAxisOrdinate": row = self.layout.row(align=True) row.label(text="Derived Angle") - row.label( - text=str( - round( - ifcopenshell.util.geolocation.xaxis2angle( - GeoreferenceData.data["map_conversion"]["XAxisAbscissa"], GeoreferenceData.data["map_conversion"]["XAxisOrdinate"] - ), - 3, - ) - ) - ) + row.label(text=GeoreferenceData.data["map_derived_angle"]) if GeoreferenceData.data["true_north"]: row = self.layout.row() @@ -166,7 +143,7 @@ class BIM_PT_gis(Panel): row.label(text=str(GeoreferenceData.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(*GeoreferenceData.data["true_north"][0:2]), 3))) + row.label(text=GeoreferenceData.data["true_derived_angle"]) class BIM_PT_gis_utilities(Panel): @@ -178,9 +155,6 @@ class BIM_PT_gis_utilities(Panel): bl_category = "BlenderBIM" def draw(self, context): - if not GeoreferenceData.is_loaded: - GeoreferenceData.load() - props = context.scene.BIMGeoreferenceProperties row = self.layout.row(align=True) @@ -193,4 +167,3 @@ class BIM_PT_gis_utilities(Panel): row = self.layout.row(align=True) row.operator("bim.convert_local_to_global", text="Local to Global") row.operator("bim.convert_global_to_local", text="Global to Local") - diff --git a/src/blenderbim/blenderbim/core/georeference.py b/src/blenderbim/blenderbim/core/georeference.py index 5096594ba1..8cde5d9b20 100644 --- a/src/blenderbim/blenderbim/core/georeference.py +++ b/src/blenderbim/blenderbim/core/georeference.py @@ -16,63 +16,67 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . + def add_georeferencing(ifc): ifc.run("georeference.add_georeferencing") + def enable_editing_georeferencing(georeference): - georeference.clear_projected_crs() georeference.import_projected_crs() - georeference.clear_map_conversion() georeference.import_map_conversion() - georeference.set_has_true_north_prop() - georeference.set_true_north_props() + georeference.import_true_north() georeference.enable_editing() - + + def remove_georeferencing(ifc): ifc.run("georeference.remove_georeferencing") + def disable_editing_georeferencing(georeference): georeference.disable_editing() - -def edit_georeferencing(ifc, georeference): - projected_crs = georeference.get_projected_crs_attributes() - map_conversion = georeference.get_map_conversion_attributes() - true_north = georeference.get_true_north_attributes() - georeference.edit_georeferencing(projected_crs, map_conversion, true_north) - georeference.disable_editing() - -def set_ifc_grid_north(georeference): - x_angle = georeference.get_x_angle_from_sun_position() - georeference.set_xaxis_vector_from_angle(x_angle) - -def set_blender_grid_north(georeference): - angle = georeference.get_angle_from_xaxis() - georeference.set_sun_pos_north_offset(angle) -def get_cursor_location(georeference): - georeference.get_cursor_location() - -def set_cursor_location(georeference): - scale = georeference.get_scale() - coordinates = georeference.get_coordinates_from_coordinate_output_prop() - georeference.set_cursor_location(coordinates, scale) + +def edit_georeferencing(ifc, georeference): + ifc.run( + "georeference.edit_georeferencing", + projected_crs=georeference.get_projected_crs_attributes(), + map_conversion=georeference.get_map_conversion_attributes(), + true_north=georeference.get_true_north_attributes(), + ) + georeference.disable_editing() + + +def set_ifc_grid_north(georeference): + georeference.set_ifc_grid_north() + + +def set_blender_grid_north(georeference): + georeference.set_blender_grid_north() + def set_ifc_true_north(georeference): georeference.set_ifc_true_north() + def set_blender_true_north(georeference): georeference.set_blender_true_north() -def convert_local_to_global(georeference): - map_conversion = georeference.get_map_conversion() - coordinates = georeference.get_easting_northing_height_from_xyz(map_conversion) - georeference.set_coordinate_output_prop(coordinates) - georeference.set_cursor_location(coordinates, scale = 1) - -def convert_global_to_local(georeference): - map_conversion = georeference.get_map_conversion() - coordinates = georeference.get_xyz_from_easting_northig_height(map_conversion) - georeference.set_coordinate_output_prop(coordinates) - scale = georeference.get_scale() - georeference.set_cursor_location(coordinates, scale) +def get_cursor_location(georeference): + georeference.set_coordinates("input", georeference.get_cursor_location()) + + +def set_cursor_location(georeference): + georeference.set_cursor_location(georeference.get_coordinates("output")) + + +def convert_local_to_global(georeference): + coordinates = georeference.xyz2enh(georeference.get_coordinates("input"), georeference.get_map_conversion()) + georeference.set_coordinates("output", coordinates) + georeference.set_cursor_location(coordinates) + + +def convert_global_to_local(georeference): + coordinates = georeference.enh2xyz(georeference.get_coordinates("input"), georeference.get_map_conversion()) + georeference.set_coordinates("output", coordinates) + georeference.set_cursor_location(coordinates) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 2858a6a943..3fa9c6808b 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -247,41 +247,29 @@ class Geometry: def should_generate_uvs(cls, obj): pass def should_use_presentation_style_assignment(cls): pass + @interface class Georeference: - def clear_projected_crs(cls): pass - def clear_map_conversion(cls): pass - def import_projected_crs(cls): pass - def import_map_conversion(cls): pass - def set_has_true_north_prop(cls): pass - def set_true_north_props(cls): pass - def enable_editing(cls): pass def disable_editing(cls): pass - def get_projected_crs_attributes(cls): pass - def get_map_conversion_attributes(cls): pass - def get_true_north_attributes(cls): pass - def edit_georeferencing(cls, projected_crs, map_conversion, true_north): pass - def get_file(cls): pass - def import_projected_crs_attributes(cls, name, prop, data): pass - def import_map_conversion_attributes(cls, name, prop, data): pass - def export_crs_attributes(cls, attributes, prop): pass - def export_map_attributes(cls, attributes, prop): pass - def set_ifc_grid_north(cls): pass - def set_blender_grid_north(cls): pass + def enable_editing(cls): pass + def enh2xyz(cls, map_conversion, coordinates): pass + def get_coordinates(cls, io): pass def get_cursor_location(cls): pass - def set_cursor_location(cls, coordinates, scale): pass - def get_scale(cls): pass - def get_coordinates_from_coordinate_output_prop(cls): pass - def set_ifc_true_north(cls): pass - def set_blender_true_north(cls): pass def get_map_conversion(cls): pass - def get_easting_northing_height_from_xyz(cls, map_conversion): pass - def get_xyz_from_easting_northig_height(cls, map_conversion): pass - def set_coordinate_output_prop(cls, coordinates): pass - def get_x_angle_from_sun_position(cls): pass - def set_xaxis_vector_from_angle(cls, x_angle): pass - def get_angle_from_xaxis(cls): pass - def set_sun_pos_north_offset(cls, angle): pass + def get_map_conversion_attributes(cls): pass + def get_projected_crs_attributes(cls): pass + def get_true_north_attributes(cls): pass + def import_map_conversion(cls): pass + def import_projected_crs(cls): pass + def import_true_north(cls): pass + def set_blender_grid_north(cls): pass + def set_blender_true_north(cls): pass + def set_coordinates(cls, io, coordinates): pass + def set_cursor_location(cls, coordinates): pass + def set_ifc_grid_north(cls): pass + def set_ifc_true_north(cls): pass + def xyz2enh(cls, map_conversion, coordinates): pass + @interface class Ifc: @@ -543,10 +531,10 @@ class Unit: def get_scene_unit_name(cls, unit_type): pass def get_scene_unit_si_prefix(cls): pass def get_si_name_from_unit_type(cls, unit_type): pass - def get_unit_class(cls, unit): pass def import_unit_attributes(cls, unit): pass def import_units(cls): pass def is_scene_unit_metric(cls): pass + def is_unit_class(cls, unit, ifc_class): pass def set_active_unit(cls, unit): pass diff --git a/src/blenderbim/blenderbim/tool/georeference.py b/src/blenderbim/blenderbim/tool/georeference.py index 14c2f0dc95..5a22ab5c42 100644 --- a/src/blenderbim/blenderbim/tool/georeference.py +++ b/src/blenderbim/blenderbim/tool/georeference.py @@ -22,210 +22,142 @@ import blenderbim.core.tool import blenderbim.tool as tool import ifcopenshell import blenderbim.bim.helper - -from ifcopenshell.api.unit.data import Data as UnitData from math import radians, degrees, atan, tan, cos, sin + class Georeference(blenderbim.core.tool.Georeference): - - @classmethod - def clear_projected_crs(cls): - bpy.context.scene.BIMGeoreferenceProperties.projected_crs.clear() - - @classmethod - def clear_map_conversion(cls): - bpy.context.scene.BIMGeoreferenceProperties.map_conversion.clear() - @classmethod def import_projected_crs(cls): - ifc = tool.Ifc.get() - projected_crs = {} - - if ifc.schema == "IFC2X3": - return - - for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): - if not context.HasCoordinateOperation: - continue - - projected_crs = context.HasCoordinateOperation[0].TargetCRS.get_info() - if projected_crs["MapUnit"]: - projected_crs["MapUnit"] = map_conversion.TargetCRS.MapUnit.get_info() + def callback(name, prop, data): + if name == "MapUnit": + new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add() + new.name = name + new.data_type = "enum" + new.is_null = data[name] is None + new.is_optional = True + new.enum_items = json.dumps( + { + u.id(): (getattr(u, "Prefix", "") or "") + u.Name + for u in tool.Ifc.get().by_type("IfcNamedUnit") + if u.UnitType == "LENGTHUNIT" + } + ) + if data["MapUnit"]: + new.enum_value = str(data["MapUnit"].id()) + return True - break - props = bpy.context.scene.BIMGeoreferenceProperties - blenderbim.bim.helper.import_attributes( - "IfcProjectedCRS", - props.projected_crs, - projected_crs, - cls.import_projected_crs_attributes, - ) - + props.projected_crs.clear() + + if tool.Ifc.get_schema() == "IFC2X3": + return + + for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if context.HasCoordinateOperation: + projected_crs = context.HasCoordinateOperation[0].TargetCRS + blenderbim.bim.helper.import_attributes2(projected_crs, props.projected_crs, callback=callback) + return + @classmethod def import_map_conversion(cls): - ifc = tool.Ifc.get() - map_conversion_v = {} - if ifc.schema == "IFC2X3": - return - - for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): - if not context.HasCoordinateOperation: - continue - - map_conversion_v = context.HasCoordinateOperation[0].get_info() - map_conversion_v["SourceCRS"] = map_conversion_v["SourceCRS"].id() - map_conversion_v["TargetCRS"] = map_conversion_v["TargetCRS"].id() - - break - - props = bpy.context.scene.BIMGeoreferenceProperties - blenderbim.bim.helper.import_attributes( - "IfcMapConversion", - props.map_conversion, - map_conversion_v, - cls.import_map_conversion_attributes, - ) - - @classmethod - def set_has_true_north_prop(cls): - ifc = tool.Ifc.get() - true_north = {} - - if ifc.schema == "IFC2X3": - return - - for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): - if not context.TrueNorth: - continue - true_north = context.TrueNorth.DirectionRatios - break + def callback(name, prop, data): + if 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 - bpy.context.scene.BIMGeoreferenceProperties.has_true_north = bool(true_north) - - @classmethod - def set_true_north_props(cls): - ifc = tool.Ifc.get() - true_north = {} - - if ifc.schema == "IFC2X3": + props = bpy.context.scene.BIMGeoreferenceProperties + props.map_conversion.clear() + + if tool.Ifc.get_schema() == "IFC2X3": return - - for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): - if not context.TrueNorth: - continue - true_north = context.TrueNorth.DirectionRatios - break - - if true_north: - bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa = str(true_north[0]) - bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate = str(true_north[1]) - + + 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) + return + + @classmethod + def import_true_north(cls): + if tool.Ifc.get_schema() == "IFC2X3": + return + + props = bpy.context.scene.BIMGeoreferenceProperties + props.has_true_north = False + for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if context.TrueNorth: + true_north = context.TrueNorth.DirectionRatios + props.true_north_abscissa = str(true_north[0]) + props.true_north_ordinate = str(true_north[1]) + props.has_true_north = True + return + @classmethod def get_projected_crs_attributes(cls): + def callback(attributes, prop): + if not prop.is_null and prop.name == "MapUnit": + attributes[prop.name] = tool.Ifc.get().by_id(int(prop.enum_value)) + return True + props = bpy.context.scene.BIMGeoreferenceProperties - projected_crs = blenderbim.bim.helper.export_attributes(props.projected_crs, cls.export_crs_attributes) - return projected_crs - + return blenderbim.bim.helper.export_attributes(props.projected_crs, callback=callback) + @classmethod def get_map_conversion_attributes(cls): + def callback(attributes, prop): + if 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 - map_conversion = blenderbim.bim.helper.export_attributes(props.map_conversion, cls.export_map_attributes) - return map_conversion - + return blenderbim.bim.helper.export_attributes(props.map_conversion, callback=callback) + @classmethod def get_true_north_attributes(cls): props = bpy.context.scene.BIMGeoreferenceProperties - true_north = None if props.has_true_north: try: - true_north = [float(props.true_north_abscissa), float(props.true_north_ordinate)] + return [float(props.true_north_abscissa), float(props.true_north_ordinate)] except ValueError: print("ERROR, True North Abscissa and Ordinate expect a number") - #self.report({"ERROR"}, "True North Abscissa and Ordinate expect a number") - return true_north - + # self.report({"ERROR"}, "True North Abscissa and Ordinate expect a number") + @classmethod def enable_editing(cls): bpy.context.scene.BIMGeoreferenceProperties.is_editing = True - + @classmethod def disable_editing(cls): bpy.context.scene.BIMGeoreferenceProperties.is_editing = False - + @classmethod - def edit_georeferencing(cls, projected_crs, map_conversion, true_north): - tool.Ifc.run( - "georeference.edit_georeferencing", - **{ - "projected_crs": projected_crs, - "map_conversion": map_conversion, - "true_north": true_north, - } - ) - + def set_coordinates(cls, io, coordinates): + if io == "input": + bpy.context.scene.BIMGeoreferenceProperties.coordinate_input = ",".join([str(o) for o in coordinates]) + elif io == "output": + bpy.context.scene.BIMGeoreferenceProperties.coordinate_output = ",".join([str(o) for o in coordinates]) + @classmethod - def get_file(cls): - return tool.Ifc.get() - - @classmethod - def import_projected_crs_attributes(cls, name, prop, data): - if name == "MapUnit": - new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add() - new.name = name - new.data_type = "enum" - new.is_null = data[name] is None - new.is_optional = True - new.enum_items = json.dumps( - {u["id"]: u["Name"] for u in UnitData.units.values() if u["UnitType"] == "LENGTHUNIT"} - ) - if data["MapUnit"]: - new.enum_value = str(data["MapUnit"]["id"]) - return True - - @classmethod - def import_map_conversion_attributes(cls, name, prop, data): - if 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 - - @classmethod - def export_crs_attributes(cls, attributes, prop): - ifc_file = tool.Ifc.get() - if not prop.is_null and prop.name == "MapUnit": - attributes[prop.name] = ifc_file.by_id(int(prop.enum_value)) - return True - - @classmethod - def export_map_attributes(cls, attributes, prop): - if 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 - + def get_coordinates(cls, io): + if io == "input": + return [float(co) for co in bpy.context.scene.BIMGeoreferenceProperties.coordinate_input.split(",")] + elif io == "output": + return [float(co) for co in bpy.context.scene.BIMGeoreferenceProperties.coordinate_output.split(",")] + @classmethod def get_cursor_location(cls): props = bpy.context.scene.BIMGeoreferenceProperties scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - project_coordinates = [o / scale for o in bpy.context.scene.cursor.location] - props.coordinate_input = ",".join([str(o) for o in project_coordinates]) - + return [o / scale for o in bpy.context.scene.cursor.location] + @classmethod - def set_cursor_location(cls, coordinates, scale): - bpy.context.scene.cursor.location = coordinates * scale - - @classmethod - def get_scale(cls): - return ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - - @classmethod - def get_coordinates_from_coordinate_output_prop(cls): - props = bpy.context.scene.BIMGeoreferenceProperties - coordinates = [float(co) for co in props.coordinate_output.split(",")] - return coordinates - + def set_cursor_location(cls, coordinates): + scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + bpy.context.scene.cursor.location = [co * scale for co in coordinates] + @classmethod def set_ifc_true_north(cls): y_angle = -bpy.context.scene.sun_pos_properties.north_offset + radians(90) @@ -236,41 +168,27 @@ class Georeference(blenderbim.core.tool.Georeference): def set_blender_true_north(cls): bpy.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), + float(bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa), + float(bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate), ) ) - + @classmethod def get_map_conversion(cls): - ifc = tool.Ifc.get() - map_conversion = {} - if ifc.schema == "IFC2X3": + if tool.Ifc.get_schema() == "IFC2X3": return - - for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): - if not context.HasCoordinateOperation: - continue - - map_conversion = context.HasCoordinateOperation[0].get_info() - map_conversion["SourceCRS"] = map_conversion["SourceCRS"].id() - map_conversion["TargetCRS"] = map_conversion["TargetCRS"].id() - - break - - return map_conversion - - @classmethod - def get_easting_northing_height_from_xyz(cls, map_conversion): - props = bpy.context.scene.BIMGeoreferenceProperties - - x, y, z = [float(co) for co in props.coordinate_input.split(",")] + for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if context.HasCoordinateOperation: + return context.HasCoordinateOperation[0] + @classmethod + def xyz2enh(cls, coordinates, map_conversion): + props = bpy.context.scene.BIMGeoreferenceProperties if props.has_blender_offset: - results = ifcopenshell.util.geolocation.xyz2enh( - x, - y, - z, + coordinates = ifcopenshell.util.geolocation.xyz2enh( + coordinates[0], + coordinates[1], + coordinates[2], float(props.blender_eastings), float(props.blender_northings), float(props.blender_orthogonal_height), @@ -278,52 +196,41 @@ class Georeference(blenderbim.core.tool.Georeference): float(props.blender_x_axis_ordinate), 1.0, ) - x, y, z = results - # TODO: what if the project CRS units and the project units are different? - - if map_conversion: #TODO return error when there is the map conversiona but no Xaxis, scale, etc.. - results = ifcopenshell.util.geolocation.xyz2enh( - x, - y, - z, - map_conversion["Eastings"], - map_conversion["Northings"], - map_conversion["OrthogonalHeight"], - map_conversion.get("XAxisAbscissa", 1.0), - map_conversion.get("XAxisOrdinate", 0.0), - map_conversion.get("Scale", 1.0), - ) - else: - results = (x, y, z) - - return results - - @classmethod - def get_xyz_from_easting_northig_height(cls, map_conversion): - props = bpy.context.scene.BIMGeoreferenceProperties - x, y, z = [float(co) for co in props.coordinate_input.split(",")] - if map_conversion: - results = ifcopenshell.util.geolocation.enh2xyz( - x, - y, - z, - map_conversion["Eastings"], - map_conversion["Northings"], - map_conversion["OrthogonalHeight"], - map_conversion.get("XAxisAbscissa", 1.0), - map_conversion.get("XAxisOrdinate", 0.0), - map_conversion.get("Scale", 1.0), + coordinates = ifcopenshell.util.geolocation.xyz2enh( + coordinates[0], + coordinates[1], + coordinates[2], + map_conversion.Eastings, + map_conversion.Northings, + map_conversion.OrthogonalHeight, + map_conversion.XAxisAbscissa or 1.0, + map_conversion.XAxisOrdinate or 0.0, + map_conversion.Scale or 1.0, ) - else: - results = (x, y, z) + return coordinates + @classmethod + def enh2xyz(cls, coordinates, map_conversion): + props = bpy.context.scene.BIMGeoreferenceProperties + if map_conversion: + coordinates = ifcopenshell.util.geolocation.enh2xyz( + coordinates[0], + coordinates[1], + coordinates[2], + map_conversion.Eastings, + map_conversion.Northings, + map_conversion.OrthogonalHeight, + map_conversion.XAxisAbscissa or 1.0, + map_conversion.XAxisOrdinate or 0.0, + map_conversion.Scale or 1.0, + ) if props.has_blender_offset: - results = ifcopenshell.util.geolocation.enh2xyz( - results[0], - results[1], - results[2], + coordinates = ifcopenshell.util.geolocation.enh2xyz( + coordinates[0], + coordinates[1], + coordinates[2], float(props.blender_eastings), float(props.blender_northings), float(props.blender_orthogonal_height), @@ -331,31 +238,20 @@ class Georeference(blenderbim.core.tool.Georeference): float(props.blender_x_axis_ordinate), 1.0, ) - - return results - - @classmethod - def set_coordinate_output_prop(cls, coordinates): - props = bpy.context.scene.BIMGeoreferenceProperties - props.coordinate_output = ",".join([str(r) for r in coordinates]) - - @classmethod - def get_x_angle_from_sun_position(cls): - return -bpy.context.scene.sun_pos_properties.north_offset - - @classmethod - def set_xaxis_vector_from_angle(cls, x_angle): - bpy.context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").string_value = str(cos(x_angle)) - bpy.context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").string_value = str(sin(x_angle)) - - @classmethod - def get_angle_from_xaxis(cls): - return ifcopenshell.util.geolocation.xaxis2angle( - float(bpy.context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").string_value), - float(bpy.context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").string_value), - ) - - @classmethod - def set_sun_pos_north_offset(cls, angle): - bpy.context.scene.sun_pos_properties.north_offset = -radians(angle) + return coordinates + @classmethod + 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)) + + @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), + ) + bpy.context.scene.sun_pos_properties.north_offset = -radians(angle) diff --git a/src/blenderbim/pytest.ini b/src/blenderbim/pytest.ini index 0f1e811706..4ad6fa92de 100644 --- a/src/blenderbim/pytest.ini +++ b/src/blenderbim/pytest.ini @@ -10,6 +10,7 @@ markers = document drawing geometry + georeference library material misc diff --git a/src/blenderbim/test/bim/feature/georeference.feature b/src/blenderbim/test/bim/feature/georeference.feature new file mode 100644 index 0000000000..85922285d6 --- /dev/null +++ b/src/blenderbim/test/bim/feature/georeference.feature @@ -0,0 +1,82 @@ +@georeference +Feature: Georeference + +Scenario: Add georeferencing + Given an empty IFC project + When I press "bim.add_georeferencing" + Then nothing happens + +Scenario: Enable editing georeferencing + Given an empty IFC project + And I press "bim.add_georeferencing" + When I press "bim.enable_editing_georeferencing" + Then nothing happens + +Scenario: Remove georeferencing + Given an empty IFC project + And I press "bim.add_georeferencing" + When I press "bim.remove_georeferencing" + Then nothing happens + +Scenario: Edit georeferencing + Given an empty IFC project + And I press "bim.add_georeferencing" + And I press "bim.enable_editing_georeferencing" + When I press "bim.edit_georeferencing" + Then nothing happens + +Scenario: Disable editing georeferencing + Given an empty IFC project + And I press "bim.add_georeferencing" + And I press "bim.enable_editing_georeferencing" + When I press "bim.disable_editing_georeferencing" + Then nothing happens + +Scenario: Set IFC grid north + Given an empty IFC project + And I press "bim.add_georeferencing" + And I press "bim.enable_editing_georeferencing" + When I press "bim.set_ifc_grid_north" + Then nothing happens + +Scenario: Set Blender grid north + Given an empty IFC project + And I press "bim.add_georeferencing" + And I press "bim.enable_editing_georeferencing" + And I press "bim.set_ifc_grid_north" + When I press "bim.set_blender_grid_north" + Then nothing happens + +Scenario: Get cursor location + Given an empty IFC project + When I press "bim.get_cursor_location" + Then nothing happens + +Scenario: Set cursor location + Given an empty IFC project + When I set "scene.BIMGeoreferenceProperties.coordinate_output" to "0,0,0" + And I press "bim.set_cursor_location" + Then nothing happens + +Scenario: Set IFC true north + Given an empty IFC project + When I press "bim.set_ifc_true_north" + Then nothing happens + +Scenario: Set Blender true north + Given an empty IFC project + And I press "bim.set_ifc_true_north" + When I press "bim.set_blender_true_north" + Then nothing happens + +Scenario: Convert local to global + Given an empty IFC project + When I set "scene.BIMGeoreferenceProperties.coordinate_input" to "0,0,0" + And I press "bim.convert_local_to_global" + Then "scene.BIMGeoreferenceProperties.coordinate_output" is "0.0,0.0,0.0" + +Scenario: Convert global to local + Given an empty IFC project + When I set "scene.BIMGeoreferenceProperties.coordinate_input" to "0,0,0" + And I press "bim.convert_global_to_local" + Then "scene.BIMGeoreferenceProperties.coordinate_output" is "0.0,0.0,0.0" diff --git a/src/blenderbim/test/core/bootstrap.py b/src/blenderbim/test/core/bootstrap.py index d18f7e815c..686d23ab65 100644 --- a/src/blenderbim/test/core/bootstrap.py +++ b/src/blenderbim/test/core/bootstrap.py @@ -98,6 +98,13 @@ def geometry(): prophet.verify() +@pytest.fixture +def georeference(): + prophet = Prophecy(blenderbim.core.tool.Georeference) + yield prophet + prophet.verify() + + @pytest.fixture def library(): prophet = Prophecy(blenderbim.core.tool.Library) diff --git a/src/blenderbim/test/core/test_georeference.py b/src/blenderbim/test/core/test_georeference.py new file mode 100644 index 0000000000..957e43b232 --- /dev/null +++ b/src/blenderbim/test/core/test_georeference.py @@ -0,0 +1,114 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2022 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blenderbim.core.georeference as subject +from test.core.bootstrap import ifc, georeference + + +class TestAddGeoreferencing: + def test_run(self, ifc): + ifc.run("georeference.add_georeferencing").should_be_called() + subject.add_georeferencing(ifc) + + +class TestEnableGeoreferencing: + def test_run(self, georeference): + georeference.import_projected_crs().should_be_called() + georeference.import_map_conversion().should_be_called() + georeference.import_true_north().should_be_called() + georeference.enable_editing().should_be_called() + subject.enable_editing_georeferencing(georeference) + + +class TestRemoveGeoreferencing: + def test_run(self, ifc): + ifc.run("georeference.remove_georeferencing").should_be_called() + subject.remove_georeferencing(ifc) + + +class TestEditGeoreferencing: + def test_run(self, ifc, georeference): + georeference.get_projected_crs_attributes().should_be_called().will_return("projected_crs_attributes") + georeference.get_map_conversion_attributes().should_be_called().will_return("map_conversion_attributes") + georeference.get_true_north_attributes().should_be_called().will_return("true_north_attributes") + ifc.run( + "georeference.edit_georeferencing", + projected_crs="projected_crs_attributes", + map_conversion="map_conversion_attributes", + true_north="true_north_attributes", + ).should_be_called() + georeference.disable_editing().should_be_called() + subject.edit_georeferencing(ifc, georeference) + + +class TestSetIfcGridNorth: + def test_run(self, georeference): + georeference.set_ifc_grid_north().should_be_called() + subject.set_ifc_grid_north(georeference) + + +class TestSetBlenderGridNorth: + def test_run(self, georeference): + georeference.set_blender_grid_north().should_be_called() + subject.set_blender_grid_north(georeference) + + +class TestSetIfcTrueNorth: + def test_run(self, georeference): + georeference.set_ifc_true_north().should_be_called() + subject.set_ifc_true_north(georeference) + + +class TestSetBlenderTrueNorth: + def test_run(self, georeference): + georeference.set_blender_true_north().should_be_called() + subject.set_blender_true_north(georeference) + + +class TestGetCursorLocation: + def test_run(self, georeference): + georeference.get_cursor_location().should_be_called().will_return("coordinates") + georeference.set_coordinates("input", "coordinates").should_be_called() + subject.get_cursor_location(georeference) + + +class TestSetCursorLocation: + def test_run(self, georeference): + georeference.get_coordinates("output").should_be_called().will_return("coordinates") + georeference.set_cursor_location("coordinates").should_be_called() + subject.set_cursor_location(georeference) + + +class TestConvertLocalToGlobal: + def test_run(self, georeference): + georeference.get_coordinates("input").should_be_called().will_return("coordinates") + georeference.get_map_conversion().should_be_called().will_return("map_conversion") + georeference.xyz2enh("coordinates", "map_conversion").should_be_called().will_return("enh") + georeference.set_coordinates("output", "enh").should_be_called() + georeference.set_cursor_location("enh").should_be_called() + subject.convert_local_to_global(georeference) + + +class TestConvertGlobalToLocal: + def test_run(self, georeference): + georeference.get_coordinates("input").should_be_called().will_return("coordinates") + georeference.get_map_conversion().should_be_called().will_return("map_conversion") + georeference.enh2xyz("coordinates", "map_conversion").should_be_called().will_return("xyz") + georeference.set_coordinates("output", "xyz").should_be_called() + georeference.set_cursor_location("xyz").should_be_called() + subject.convert_global_to_local(georeference) diff --git a/src/blenderbim/test/tool/test_georeference.py b/src/blenderbim/test/tool/test_georeference.py new file mode 100644 index 0000000000..028ab83d44 --- /dev/null +++ b/src/blenderbim/test/tool/test_georeference.py @@ -0,0 +1,357 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import math +import ifcopenshell +import blenderbim.core.tool +import blenderbim.tool as tool +from mathutils import Vector +from test.bim.bootstrap import NewFile +from blenderbim.tool.georeference import Georeference as subject + + +class TestImplementsTool(NewFile): + def test_run(self): + assert isinstance(subject(), blenderbim.core.tool.Georeference) + + +class TestImportProjectedCRS(NewFile): + def test_importing_nothing_with_no_georeferencing(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + ifcopenshell.api.run("context.add_context", ifc, context_type="Model") + subject.import_projected_crs() + props = bpy.context.scene.BIMGeoreferenceProperties + assert len(props.projected_crs) == 0 + + def test_importing_projected_crs(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + ifcopenshell.api.run("context.add_context", ifc, context_type="Model") + ifcopenshell.api.run("georeference.add_georeferencing", ifc) + projected_crs = ifc.by_type("IfcProjectedCRS")[0] + projected_crs.Name = "Name" + projected_crs.Description = "Description" + projected_crs.GeodeticDatum = "GeodeticDatum" + projected_crs.VerticalDatum = "VerticalDatum" + projected_crs.MapProjection = "MapProjection" + projected_crs.MapZone = "MapZone" + unit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", name="METRE") + projected_crs.MapUnit = unit + subject.import_projected_crs() + props = bpy.context.scene.BIMGeoreferenceProperties + assert props.projected_crs.get("Name").string_value == "Name" + assert props.projected_crs.get("Description").string_value == "Description" + assert props.projected_crs.get("GeodeticDatum").string_value == "GeodeticDatum" + assert props.projected_crs.get("VerticalDatum").string_value == "VerticalDatum" + assert props.projected_crs.get("MapProjection").string_value == "MapProjection" + assert props.projected_crs.get("MapZone").string_value == "MapZone" + assert props.projected_crs.get("MapUnit").enum_value == str(unit.id()) + + def test_run_ifc2x3(self): + ifc = ifcopenshell.file(schema="IFC2X3") + tool.Ifc.set(ifc) + subject.import_projected_crs() + props = bpy.context.scene.BIMGeoreferenceProperties + assert len(props.projected_crs) == 0 + + +class TestImportMapConversion(NewFile): + def test_importing_nothing_with_no_georeferencing(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + ifcopenshell.api.run("context.add_context", ifc, context_type="Model") + subject.import_map_conversion() + props = bpy.context.scene.BIMGeoreferenceProperties + assert len(props.map_conversion) == 0 + + def test_importing_map_conversion(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + ifcopenshell.api.run("context.add_context", ifc, context_type="Model") + ifcopenshell.api.run("georeference.add_georeferencing", ifc) + map_conversion = ifc.by_type("IfcMapConversion")[0] + map_conversion.Eastings = 1 + map_conversion.Northings = 2 + map_conversion.OrthogonalHeight = 3 + map_conversion.XAxisAbscissa = 4 + map_conversion.XAxisOrdinate = 5 + map_conversion.Scale = 6 + subject.import_map_conversion() + props = bpy.context.scene.BIMGeoreferenceProperties + assert props.map_conversion.get("Eastings").string_value == "1.0" + assert props.map_conversion.get("Northings").string_value == "2.0" + assert props.map_conversion.get("OrthogonalHeight").string_value == "3.0" + assert props.map_conversion.get("XAxisAbscissa").string_value == "4.0" + assert props.map_conversion.get("XAxisOrdinate").string_value == "5.0" + assert props.map_conversion.get("Scale").string_value == "6.0" + + def test_run_ifc2x3(self): + ifc = ifcopenshell.file(schema="IFC2X3") + tool.Ifc.set(ifc) + subject.import_map_conversion() + props = bpy.context.scene.BIMGeoreferenceProperties + assert len(props.map_conversion) == 0 + + +class TestImportTrueNorth(NewFile): + def test_detecting_no_true_north(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + context = ifcopenshell.api.run("context.add_context", ifc, context_type="Model") + subject.import_true_north() + props = bpy.context.scene.BIMGeoreferenceProperties + assert props.has_true_north is False + + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + context = ifcopenshell.api.run("context.add_context", ifc, context_type="Model") + context.TrueNorth = ifc.createIfcDirection((1.0, 2.0, 0.0)) + subject.import_true_north() + props = bpy.context.scene.BIMGeoreferenceProperties + assert props.true_north_abscissa == "1.0" + assert props.true_north_ordinate == "2.0" + assert props.has_true_north is True + + +class TestGetProjectedCRSAttributes(NewFile): + def test_run(self): + TestImportProjectedCRS().test_importing_projected_crs() + assert subject.get_projected_crs_attributes() == { + "Name": "Name", + "Description": "Description", + "GeodeticDatum": "GeodeticDatum", + "VerticalDatum": "VerticalDatum", + "MapProjection": "MapProjection", + "MapZone": "MapZone", + "MapUnit": tool.Ifc.get().by_type("IfcNamedUnit")[0], + } + + +class TestGetMapConversionAttributes(NewFile): + def test_run(self): + TestImportMapConversion().test_importing_map_conversion() + assert subject.get_map_conversion_attributes() == { + "Eastings": 1.0, + "Northings": 2.0, + "OrthogonalHeight": 3.0, + "XAxisAbscissa": 4.0, + "XAxisOrdinate": 5.0, + "Scale": 6.0, + } + +class TestGetTrueNorthAttributes(NewFile): + def test_run(self): + TestImportTrueNorth().test_run() + assert subject.get_true_north_attributes() == [1.0, 2.0] + + +class TestEnableEditing(NewFile): + def test_run(self): + bpy.context.scene.BIMGeoreferenceProperties.is_editing = False + subject.enable_editing() + assert bpy.context.scene.BIMGeoreferenceProperties.is_editing is True + + +class TestDisableEditing(NewFile): + def test_run(self): + bpy.context.scene.BIMGeoreferenceProperties.is_editing = True + subject.disable_editing() + assert bpy.context.scene.BIMGeoreferenceProperties.is_editing is False + + +class TestSetCoordinates(NewFile): + def test_run(self): + subject.set_coordinates("input", [1., 2., 3.]) + assert bpy.context.scene.BIMGeoreferenceProperties.coordinate_input == "1.0,2.0,3.0" + subject.set_coordinates("output", [4., 5., 6.]) + assert bpy.context.scene.BIMGeoreferenceProperties.coordinate_output == "4.0,5.0,6.0" + + +class TestGetCoordinates(NewFile): + def test_run(self): + bpy.context.scene.BIMGeoreferenceProperties.coordinate_input = "1.0,2.0,3.0" + assert subject.get_coordinates("input") == [1., 2., 3.] + bpy.context.scene.BIMGeoreferenceProperties.coordinate_output = "4.0,5.0,6.0" + assert subject.get_coordinates("output") == [4., 5., 6.] + + +class TestGetCursorLocation(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + ifcopenshell.api.run("context.add_context", ifc, context_type="Model") + unit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", prefix="MILLI", name="METRE") + ifcopenshell.api.run("unit.assign_unit", ifc, units=[unit]) + bpy.context.scene.cursor.location = (1., 2., 3.) + assert subject.get_cursor_location() == [1000., 2000., 3000.] + + +class TestSetCursorLocation(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + ifcopenshell.api.run("context.add_context", ifc, context_type="Model") + unit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", prefix="MILLI", name="METRE") + ifcopenshell.api.run("unit.assign_unit", ifc, units=[unit]) + subject.set_cursor_location([1000., 2000., 3000.]) + assert list(bpy.context.scene.cursor.location) == [1., 2., 3.] + + +class TestSetIfcTrueNorth(NewFile): + def test_run(self): + subject.set_ifc_true_north() + assert round(float(bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa), 3) == 0.0 + assert round(float(bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate), 3) == 1.0 + bpy.context.scene.sun_pos_properties.north_offset = math.radians(45) + subject.set_ifc_true_north() + assert round(float(bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa), 3) == 0.707 + assert round(float(bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate), 3) == 0.707 + + +class TestSetBlenderTrueNorth(NewFile): + def test_run(self): + bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa = "0.0" + bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate = "1.0" + subject.set_blender_true_north() + assert bpy.context.scene.sun_pos_properties.north_offset == 0 + bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa = "0.707" + bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate = "0.707" + subject.set_blender_true_north() + assert round(math.degrees(bpy.context.scene.sun_pos_properties.north_offset)) == 45 + + +class TestGetMapConversion(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + ifcopenshell.api.run("context.add_context", ifc, context_type="Model") + ifcopenshell.api.run("georeference.add_georeferencing", ifc) + assert subject.get_map_conversion().is_a("IfcMapConversion") + + +class TestXyz2Enh(NewFile): + def test_run(self): + assert subject.xyz2enh([0., 0., 0.], None) == [0., 0., 0.] + + def test_using_the_blender_offset(self): + props = bpy.context.scene.BIMGeoreferenceProperties + props.has_blender_offset = True + props.blender_eastings = "1.0" + assert subject.xyz2enh([0., 0., 0.], None) == (1., 0., 0.) + + def test_using_the_map_conversion(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + ifcopenshell.api.run("context.add_context", ifc, context_type="Model") + ifcopenshell.api.run("georeference.add_georeferencing", ifc) + map_conversion = ifc.by_type("IfcMapConversion")[0] + map_conversion.Eastings = 1.0 + assert subject.xyz2enh([0., 0., 0.], map_conversion) == (1., 0., 0.) + + def test_applying_both_blender_offset_and_map_conversion(self): + props = bpy.context.scene.BIMGeoreferenceProperties + props.has_blender_offset = True + props.blender_eastings = "1.0" + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + ifcopenshell.api.run("context.add_context", ifc, context_type="Model") + ifcopenshell.api.run("georeference.add_georeferencing", ifc) + map_conversion = ifc.by_type("IfcMapConversion")[0] + map_conversion.Northings = 1.0 + assert subject.xyz2enh([0., 0., 0.], map_conversion) == (1., 1., 0.) + + +class TestEnh2Xyz(NewFile): + def test_run(self): + assert subject.enh2xyz([0., 0., 0.], None) == [0., 0., 0.] + + def test_using_the_blender_offset(self): + props = bpy.context.scene.BIMGeoreferenceProperties + props.has_blender_offset = True + props.blender_eastings = "1.0" + assert subject.enh2xyz([0., 0., 0.], None) == (-1., 0., 0.) + + def test_using_the_map_conversion(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + ifcopenshell.api.run("context.add_context", ifc, context_type="Model") + ifcopenshell.api.run("georeference.add_georeferencing", ifc) + map_conversion = ifc.by_type("IfcMapConversion")[0] + map_conversion.Eastings = 1.0 + assert subject.enh2xyz([0., 0., 0.], map_conversion) == (-1., 0., 0.) + + def test_applying_both_blender_offset_and_map_conversion(self): + props = bpy.context.scene.BIMGeoreferenceProperties + props.has_blender_offset = True + props.blender_eastings = "1.0" + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + ifcopenshell.api.run("context.add_context", ifc, context_type="Model") + ifcopenshell.api.run("georeference.add_georeferencing", ifc) + map_conversion = ifc.by_type("IfcMapConversion")[0] + map_conversion.Northings = 1.0 + assert subject.enh2xyz([0., 0., 0.], map_conversion) == (-1., -1., 0.) + + +class TestSetIfcGridNorth(NewFile): + def test_run(self): + props = bpy.context.scene.BIMGeoreferenceProperties + new = props.map_conversion.add() + new.name = "XAxisAbscissa" + new = props.map_conversion.add() + new.name = "XAxisOrdinate" + subject.set_ifc_grid_north() + assert round(float(props.map_conversion.get("XAxisAbscissa").string_value), 3) == 1.0 + assert round(float(props.map_conversion.get("XAxisOrdinate").string_value), 3) == 0.0 + bpy.context.scene.sun_pos_properties.north_offset = math.radians(45) + subject.set_ifc_grid_north() + assert round(float(props.map_conversion.get("XAxisAbscissa").string_value), 3) == 0.707 + assert round(float(props.map_conversion.get("XAxisOrdinate").string_value), 3) == -0.707 + + +class TestSetBlenderGridNorth(NewFile): + def test_run(self): + props = bpy.context.scene.BIMGeoreferenceProperties + new = props.map_conversion.add() + new.name = "XAxisAbscissa" + new = props.map_conversion.add() + new.name = "XAxisOrdinate" + props.map_conversion.get("XAxisAbscissa").string_value = "1.0" + props.map_conversion.get("XAxisOrdinate").string_value = "0.0" + subject.set_blender_grid_north() + assert bpy.context.scene.sun_pos_properties.north_offset == 0 + props.map_conversion.get("XAxisAbscissa").string_value = "0.707" + props.map_conversion.get("XAxisOrdinate").string_value = "-0.707" + subject.set_blender_grid_north() + assert round(math.degrees(bpy.context.scene.sun_pos_properties.north_offset)) == 45