Refactor Georeference Module

This commit is contained in:
maxfb87
2022-04-12 22:13:55 +02:00
committed by Dion Moult
parent d2ad4b2b68
commit 4b426e1fc0
7 changed files with 703 additions and 334 deletions
@@ -0,0 +1,92 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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(),
}
cls.is_loaded = True
@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
@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()
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
@@ -17,345 +17,146 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import json
import ifcopenshell
import ifcopenshell.util.unit
import ifcopenshell.util.attribute
import ifcopenshell.api
import blenderbim.bim.helper
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.georeference.data import Data
from ifcopenshell.api.unit.data import Data as UnitData
from math import radians, degrees, atan, tan, cos, sin
import blenderbim.tool as tool
import blenderbim.core.georeference as core
class EnableEditingGeoreferencing(bpy.types.Operator):
bl_idname = "bim.enable_editing_georeferencing"
bl_label = "Enable Editing Georeferencing"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.file = IfcStore.get_file()
props = context.scene.BIMGeoreferenceProperties
self.props = props
props.projected_crs.clear()
blenderbim.bim.helper.import_attributes(
"IfcProjectedCRS", props.projected_crs, Data.projected_crs, self.import_projected_crs_attributes
)
props.map_conversion.clear()
blenderbim.bim.helper.import_attributes(
"IfcMapConversion", props.map_conversion, Data.map_conversion, self.import_map_conversion_attributes
)
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"}
def import_projected_crs_attributes(self, name, prop, data):
if name == "MapUnit":
new = self.props.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
def import_map_conversion_attributes(self, 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
class DisableEditingGeoreferencing(bpy.types.Operator):
bl_idname = "bim.disable_editing_georeferencing"
bl_label = "Disable Editing Georeferencing"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMGeoreferenceProperties
props.is_editing = False
return {"FINISHED"}
class EditGeoreferencing(bpy.types.Operator):
bl_idname = "bim.edit_georeferencing"
bl_label = "Edit Georeferencing"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
self.file = IfcStore.get_file()
props = context.scene.BIMGeoreferenceProperties
projected_crs = blenderbim.bim.helper.export_attributes(props.projected_crs, self.export_crs_attributes)
map_conversion = blenderbim.bim.helper.export_attributes(props.map_conversion, self.export_map_attributes)
true_north = None
if props.has_true_north:
try:
true_north = [float(props.true_north_abscissa), float(props.true_north_ordinate)]
except ValueError:
self.report({"ERROR"}, "True North Abscissa and Ordinate expect a number")
ifcopenshell.api.run(
"georeference.edit_georeferencing",
self.file,
**{
"map_conversion": map_conversion,
"projected_crs": projected_crs,
"true_north": true_north,
}
)
Data.load(self.file)
bpy.ops.bim.disable_editing_georeferencing()
return {"FINISHED"}
def export_map_attributes(self, 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 export_crs_attributes(self, attributes, prop):
if not prop.is_null and prop.name == "MapUnit":
attributes[prop.name] = self.file.by_id(int(prop.enum_value))
return True
class SetBlenderGridNorth(bpy.types.Operator):
bl_idname = "bim.set_blender_grid_north"
bl_label = "Set Blender Grid North"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
context.scene.sun_pos_properties.north_offset = -radians(
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 SetIfcGridNorth(bpy.types.Operator):
bl_idname = "bim.set_ifc_grid_north"
bl_label = "Set IFC Grid North"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
x_angle = -context.scene.sun_pos_properties.north_offset
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"
bl_options = {"REGISTER", "UNDO"}
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"
bl_options = {"REGISTER", "UNDO"}
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"}
class RemoveGeoreferencing(bpy.types.Operator):
bl_idname = "bim.remove_georeferencing"
bl_label = "Remove Georeferencing"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
ifcopenshell.api.run("georeference.remove_georeferencing", IfcStore.get_file())
Data.load(IfcStore.get_file())
return {"FINISHED"}
class AddGeoreferencing(bpy.types.Operator):
class AddGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_georeferencing"
bl_label = "Add Georeferencing"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
bl_description = "Add a new georeference"
def _execute(self, context):
ifcopenshell.api.run("georeference.add_georeferencing", IfcStore.get_file())
Data.load(IfcStore.get_file())
return {"FINISHED"}
class ConvertLocalToGlobal(bpy.types.Operator):
bl_idname = "bim.convert_local_to_global"
bl_label = "Convert Local To Global"
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)
@classmethod
def poll(cls, context):
file = IfcStore.get_file()
props = context.scene.BIMGeoreferenceProperties
return file and props.coordinate_input.count(",") == 2
def execute(self, context):
if not Data.is_loaded:
Data.load(IfcStore.get_file())
props = context.scene.BIMGeoreferenceProperties
x, y, z = [float(co) for co in props.coordinate_input.split(",")]
if props.has_blender_offset:
results = ifcopenshell.util.geolocation.xyz2enh(
x,
y,
z,
float(props.blender_eastings),
float(props.blender_northings),
float(props.blender_orthogonal_height),
float(props.blender_x_axis_abscissa),
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 Data.map_conversion:
results = ifcopenshell.util.geolocation.xyz2enh(
x,
y,
z,
Data.map_conversion["Eastings"],
Data.map_conversion["Northings"],
Data.map_conversion["OrthogonalHeight"],
Data.map_conversion.get("XAxisAbscissa", 1.0),
Data.map_conversion.get("XAxisOrdinate", 0.0),
Data.map_conversion.get("Scale", 1.0),
)
else:
results = (x, y, z)
props.coordinate_output = ",".join([str(r) for r in results])
context.scene.cursor.location = results
return {"FINISHED"}
class ConvertGlobalToLocal(bpy.types.Operator):
bl_idname = "bim.convert_global_to_local"
bl_label = "Convert Global To Local"
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"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Close editing panel"
@classmethod
def poll(cls, context):
file = IfcStore.get_file()
props = context.scene.BIMGeoreferenceProperties
return file and file.by_type("IfcUnitAssignment") and props.coordinate_input.count(",") == 2
def _execute(self, context):
core.disable_editing_georeferencing(tool.Georeference)
def execute(self, context):
if not Data.is_loaded:
Data.load(IfcStore.get_file())
props = context.scene.BIMGeoreferenceProperties
x, y, z = [float(co) for co in props.coordinate_input.split(",")]
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)
if Data.map_conversion:
results = ifcopenshell.util.geolocation.enh2xyz(
x,
y,
z,
Data.map_conversion["Eastings"],
Data.map_conversion["Northings"],
Data.map_conversion["OrthogonalHeight"],
Data.map_conversion.get("XAxisAbscissa", 1.0),
Data.map_conversion.get("XAxisOrdinate", 0.0),
Data.map_conversion.get("Scale", 1.0),
)
else:
results = (x, y, z)
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)
if props.has_blender_offset:
results = ifcopenshell.util.geolocation.enh2xyz(
results[0],
results[1],
results[2],
float(props.blender_eastings),
float(props.blender_northings),
float(props.blender_orthogonal_height),
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
1.0,
)
props.coordinate_output = ",".join([str(r) for r in results])
scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
context.scene.cursor.location = [o * scale for o in results]
return {"FINISHED"}
class GetCursorLocation(bpy.types.Operator):
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):
file = IfcStore.get_file()
return file and file.by_type("IfcUnitAssignment")
def execute(self, context):
props = context.scene.BIMGeoreferenceProperties
scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
project_coordinates = [o / scale for o in context.scene.cursor.location]
props.coordinate_input = ",".join([str(o) for o in project_coordinates])
return {"FINISHED"}
class SetCursorLocation(bpy.types.Operator):
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"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Move curson location to the specified coordinates"
@classmethod
def poll(cls, context):
file = IfcStore.get_file()
file = tool.Ifc.get()
props = context.scene.BIMGeoreferenceProperties
return file and file.by_type("IfcUnitAssignment") 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"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Set IFC True north"
def execute(self, context):
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"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Set Blender true north"
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
scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
context.scene.cursor.location = [float(co) * scale for co in props.coordinate_output.split(",")]
return {"FINISHED"}
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)
@@ -18,10 +18,9 @@
import ifcopenshell.util.geolocation
from bpy.types import Panel
from ifcopenshell.api.georeference.data import Data
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"
@@ -31,18 +30,15 @@ class BIM_PT_gis(Panel):
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_geometry"
@classmethod
def poll(cls, context):
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())
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
if props.is_editing:
return self.draw_editable_ui(context)
self.draw_ui(context)
@@ -79,15 +75,16 @@ 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 Data.projected_crs:
if not GeoreferenceData.data["projected_crs"]:
row = self.layout.row(align=True)
row.label(text="Not Georeferenced")
if IfcStore.get_file().schema != "IFC2X3":
if IfcStore.get_file().schema != "IFC2X3": #TODO Is it correct to use IfcStore.get_file() or is it better tool.Ifc.get()?
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")
@@ -120,13 +117,13 @@ class BIM_PT_gis(Panel):
)
)
if Data.projected_crs:
if GeoreferenceData.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():
for key, value in GeoreferenceData.data["projected_crs"].items():
if key == "id" or key == "type" or not value:
continue
if key == "MapUnit":
@@ -137,11 +134,11 @@ class BIM_PT_gis(Panel):
row.label(text=key)
row.label(text=str(value))
if Data.map_conversion:
if GeoreferenceData.data["map_conversion"]:
row = self.layout.row(align=True)
row.label(text="Map Conversion", icon="GRID")
for key, value in Data.map_conversion.items():
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:
continue
row = self.layout.row(align=True)
@@ -154,22 +151,22 @@ class BIM_PT_gis(Panel):
text=str(
round(
ifcopenshell.util.geolocation.xaxis2angle(
Data.map_conversion["XAxisAbscissa"], Data.map_conversion["XAxisOrdinate"]
GeoreferenceData.data["map_conversion"]["XAxisAbscissa"], GeoreferenceData.data["map_conversion"]["XAxisOrdinate"]
),
3,
)
)
)
if Data.true_north:
if GeoreferenceData.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.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(*Data.true_north[0:2]), 3)))
row.label(text=str(round(ifcopenshell.util.geolocation.yaxis2angle(*GeoreferenceData.data["true_north"][0:2]), 3)))
class BIM_PT_gis_utilities(Panel):
@@ -181,6 +178,9 @@ 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,3 +193,4 @@ 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")
@@ -0,0 +1,78 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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.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 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)
+35
View File
@@ -247,6 +247,41 @@ 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 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
@interface
class Ifc:
@@ -28,6 +28,7 @@ from blenderbim.tool.demo import Demo
from blenderbim.tool.document import Document
from blenderbim.tool.drawing import Drawing
from blenderbim.tool.geometry import Geometry
from blenderbim.tool.georeference import Georeference
from blenderbim.tool.ifc import Ifc
from blenderbim.tool.library import Library
from blenderbim.tool.material import Material
@@ -0,0 +1,361 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
import bpy
import json
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()
break
props = bpy.context.scene.BIMGeoreferenceProperties
blenderbim.bim.helper.import_attributes(
"IfcProjectedCRS",
props.projected_crs,
projected_crs,
cls.import_projected_crs_attributes,
)
@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
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":
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])
@classmethod
def get_projected_crs_attributes(cls):
props = bpy.context.scene.BIMGeoreferenceProperties
projected_crs = blenderbim.bim.helper.export_attributes(props.projected_crs, cls.export_crs_attributes)
return projected_crs
@classmethod
def get_map_conversion_attributes(cls):
props = bpy.context.scene.BIMGeoreferenceProperties
map_conversion = blenderbim.bim.helper.export_attributes(props.map_conversion, cls.export_map_attributes)
return map_conversion
@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)]
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
@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,
}
)
@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
@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])
@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
@classmethod
def set_ifc_true_north(cls):
y_angle = -bpy.context.scene.sun_pos_properties.north_offset + radians(90)
bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa = str(cos(y_angle))
bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate = str(sin(y_angle))
@classmethod
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),
)
)
@classmethod
def get_map_conversion(cls):
ifc = tool.Ifc.get()
map_conversion = {}
if ifc.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(",")]
if props.has_blender_offset:
results = ifcopenshell.util.geolocation.xyz2enh(
x,
y,
z,
float(props.blender_eastings),
float(props.blender_northings),
float(props.blender_orthogonal_height),
float(props.blender_x_axis_abscissa),
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),
)
else:
results = (x, y, z)
if props.has_blender_offset:
results = ifcopenshell.util.geolocation.enh2xyz(
results[0],
results[1],
results[2],
float(props.blender_eastings),
float(props.blender_northings),
float(props.blender_orthogonal_height),
float(props.blender_x_axis_abscissa),
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)