diff --git a/src/blenderbim/blenderbim/bim/module/georeference/data.py b/src/blenderbim/blenderbim/bim/module/georeference/data.py index 839f3cda02..2301b91cd0 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/data.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/data.py @@ -21,6 +21,7 @@ import bpy import numpy as np import blenderbim.tool as tool import ifcopenshell.util.geolocation +from mathutils import Matrix from ifcopenshell.util.doc import get_entity_doc @@ -162,7 +163,12 @@ class GeoreferenceData: if np.allclose(wcs, np.eye(4)): result["has_transformation"] = False else: + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) result["has_transformation"] = True result["rotation"] = str(round(ifcopenshell.util.geolocation.yaxis2angle(*wcs[:, 1][:2]), 3)) result["x"], result["y"], result["z"] = wcs[:, 3][:3] + wcs[0][3] *= unit_scale + wcs[1][3] *= unit_scale + wcs[2][3] *= unit_scale + result["matrix"] = Matrix(wcs) return result diff --git a/src/blenderbim/blenderbim/bim/module/georeference/decorator.py b/src/blenderbim/blenderbim/bim/module/georeference/decorator.py new file mode 100644 index 0000000000..ed6175265d --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/georeference/decorator.py @@ -0,0 +1,298 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2024 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 blf +import gpu +import bmesh +import ifcopenshell +import blenderbim.tool as tool +from math import radians +from bpy.types import SpaceView3D +from mathutils import Vector, Matrix +from gpu_extras.batch import batch_for_shader +from bpy_extras.view3d_utils import location_3d_to_region_2d +from blenderbim.bim.module.georeference.data import GeoreferenceData + + +class GeoreferenceDecorator: + is_installed = False + handlers = [] + + @classmethod + def install(cls, context): + if cls.is_installed: + cls.uninstall() + handler = cls() + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL")) + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW")) + cls.is_installed = True + + @classmethod + def uninstall(cls): + for handler in cls.handlers: + try: + SpaceView3D.draw_handler_remove(handler, "WINDOW") + except ValueError: + pass + cls.is_installed = False + + def draw_batch(self, shader_type, content_pos, color, indices=None): + shader = self.line_shader if shader_type == "LINES" else self.shader + batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) + shader.uniform_float("color", color) + batch.draw(shader) + + def draw_text(self, context): + self.calculate_angles(context) + props = context.scene.BIMGeoreferenceProperties + e, n, h = [round(float(o), 7) for o in props.model_origin.split(",")] + + wcs_matrix = None + if GeoreferenceData.data["world_coordinate_system"]["has_transformation"]: + wcs_matrix = GeoreferenceData.data["world_coordinate_system"]["matrix"] + + self.addon_prefs = tool.Blender.get_addon_preferences() + + self.font_id = 0 + blf.size(self.font_id, 12) + color = self.addon_prefs.decorations_colour + blf.color(self.font_id, *color) + + text = "X: 0, Y: 0, Z: 0" + self.draw_text_at_position(context, text, Vector((0, -0.1, 0))) + + angle = Matrix.Rotation(radians(self.pn_angle), 4, "Z") + p = angle @ Vector((0, 1.55, 0)) + self.draw_text_at_position(context, "Project North", p) + + if not tool.Cad.is_x(self.pn_angle, 0): + arc_start = Vector((0, 1.05, 0)) + angle_half = Matrix.Rotation(radians(self.pn_angle / 2), 4, "Z") + arc_mid = angle_half @ arc_start + self.draw_text_at_position(context, f"{self.pn_angle}deg", arc_mid) + + if GeoreferenceData.data["coordinate_operation"]: + angle = Matrix.Rotation(radians(self.gn_angle), 4, "Z") + grid_north_p = angle @ Vector((0, 1.05, 0)) + self.draw_text_at_position(context, "Grid North", grid_north_p) + + arc_start = Vector((0, 0.55, 0)) + angle_half = Matrix.Rotation(radians(self.gn_angle / 2), 4, "Z") + arc_mid = angle_half @ arc_start + self.draw_text_at_position(context, f"{self.gn_angle}deg", arc_mid) + + text = f"\nE: {e}, N: {n}, H: {h}" + self.draw_text_at_position(context, text, Vector((0, -0.1, 0))) + + if GeoreferenceData.data["true_north"]: + angle = Matrix.Rotation(radians(self.tn_angle), 4, "Z") + grid_north_p = angle @ Vector((0, 0.8, 0)) + self.draw_text_at_position(context, "True North", grid_north_p) + + arc_start = Vector((0, 0.3, 0)) + angle_half = Matrix.Rotation(radians(self.tn_angle / 2), 4, "Z") + arc_mid = angle_half @ arc_start + self.draw_text_at_position(context, f"{self.tn_angle}deg", arc_mid) + + if wcs_matrix: + x, y, z = wcs_matrix.translation + if operation := GeoreferenceData.data["coordinate_operation"]: + e = operation.get("Eastings") + n = operation.get("Northings") + h = operation.get("OrthogonalHeight") + text = f"WCS\n X: {x}, Y: {y}, Z: {z}\nE: {e}, N: {n}, H: {h}" + else: + text = f"WCS\n X: {x}, Y: {y}, Z: {z}" + + if wcs_matrix.translation.length < 1000: + position = wcs_matrix.translation.copy() + else: + position = wcs_matrix.translation.normalized() * 3 + text += "\n(Warning: Actual XYZ Not Shown)" + position -= Vector((0, 0.1, 0)) + + self.draw_text_at_position(context, text, position) + + def draw_text_at_position(self, context, text, position): + coords_2d = location_3d_to_region_2d(context.region, context.region_data, position) + if not coords_2d: + return + for i, line in enumerate(text.split("\n")): + w, h = blf.dimensions(self.font_id, line) + co = coords_2d.copy() + co -= Vector((w * 0.5, 15 * i)) + blf.position(self.font_id, co[0], co[1], 0) + blf.draw(self.font_id, line) + + def draw_geometry(self, context): + self.calculate_angles(context) + + wcs_matrix = None + if GeoreferenceData.data["world_coordinate_system"]["has_transformation"]: + wcs_matrix = GeoreferenceData.data["world_coordinate_system"]["matrix"] + + self.addon_prefs = tool.Blender.get_addon_preferences() + decorator_color = self.addon_prefs.decorations_colour + decorator_color_special = self.addon_prefs.decorator_color_special + decorator_color_selected = self.addon_prefs.decorator_color_selected + decorator_color_error = self.addon_prefs.decorator_color_error + + gpu.state.blend_set("ALPHA") + + self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR") + self.line_shader.bind() # required to be able to change uniforms of the shader + # POLYLINE_UNIFORM_COLOR specific uniforms + self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height)) + + # general shader + self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") + + # A crosshair to emphasize the coordinate system, in case they have grids turned off + self.line_shader.uniform_float("lineWidth", 2.0) + verts = [Vector((0, -1.25, 0)), Vector((0, 1.25, 0)), Vector((-1.25, 0, 0)), Vector((1.25, 0, 0))] + edges = [[0, 1], [2, 3]] + self.draw_batch("LINES", verts, decorator_color, edges) + self.line_shader.uniform_float("lineWidth", 5.0) + + # A point at the origin to emphasize its significance as a Blender origin + points = [Vector((0, 0, 0))] + gpu.state.point_size_set(10) + self.draw_batch("POINTS", points, decorator_color_selected) + + # A project north arrow always exists + angle = Matrix.Rotation(radians(self.pn_angle), 4, "Z") + points = [angle @ Vector((0, 1.5, 0))] + gpu.state.point_size_set(4) + self.draw_batch("POINTS", points, decorator_color_selected) + + verts = [Vector((0, 0, 0)), angle @ Vector((0, 1.5, 0))] + edges = [[0, 1]] + self.draw_batch("LINES", verts, decorator_color_selected, edges) + + verts = [Vector((0, 1.5, 0)), Vector((-0.15, 1.35, 0)), Vector((0.15, 1.35, 0))] + edges = [[0, 1], [0, 2]] + verts = [angle @ v for v in verts] + self.draw_batch("LINES", verts, decorator_color_selected, edges) + + self.line_shader.uniform_float("lineWidth", 2.0) + + # If we have a Blender offset, project north may not be up + if not tool.Cad.is_x(self.pn_angle, 0): + arc_start = Vector((0, 1, 0)) + arc_end = angle @ arc_start + angle_half = Matrix.Rotation(radians(self.pn_angle / 2), 4, "Z") + arc_mid = angle_half @ arc_start + arc_segments = tool.Cad.create_arc_segments( + pts=[arc_start, arc_mid, arc_end], num_verts=12, make_edges=True + ) + verts, edges = arc_segments + self.draw_batch("LINES", verts, decorator_color_selected, edges) + + if GeoreferenceData.data["coordinate_operation"]: + points = [Vector((0, 0, 0)), Vector((0, 1, 0))] + angle = Matrix.Rotation(radians(self.gn_angle), 4, "Z") + points = [angle @ v for v in points] + self.draw_batch("POINTS", points, decorator_color_special) + + verts = [Vector((0, 0, 0)), Vector((0, 1, 0))] + edges = [[0, 1]] + angle = Matrix.Rotation(radians(self.gn_angle), 4, "Z") + verts = [angle @ v for v in verts] + self.draw_batch("LINES", verts, decorator_color_special, edges) + + verts = [Vector((0, 1, 0)), Vector((-0.15, 0.85, 0)), Vector((0.15, 0.85, 0))] + edges = [[0, 1], [0, 2]] + verts = [angle @ v for v in verts] + self.line_shader.uniform_float("lineWidth", 5.0) + self.draw_batch("LINES", verts, decorator_color_special, edges) + self.line_shader.uniform_float("lineWidth", 2.0) + + arc_start = Vector((0, 0.5, 0)) + arc_end = angle @ arc_start + angle_half = Matrix.Rotation(radians(self.gn_angle / 2), 4, "Z") + arc_mid = angle_half @ arc_start + arc_segments = tool.Cad.create_arc_segments( + pts=[arc_start, arc_mid, arc_end], num_verts=12, make_edges=True + ) + verts, edges = arc_segments + self.draw_batch("LINES", verts, decorator_color_special, edges) + + if GeoreferenceData.data["true_north"]: + points = [Vector((0, 0, 0)), Vector((0, 0.75, 0))] + angle = Matrix.Rotation(radians(self.tn_angle), 4, "Z") + points = [angle @ v for v in points] + self.draw_batch("POINTS", points, decorator_color_special) + + verts = [Vector((0, 0, 0)), Vector((0, 0.75, 0))] + edges = [[0, 1]] + angle = Matrix.Rotation(radians(self.tn_angle), 4, "Z") + verts = [angle @ v for v in verts] + self.draw_batch("LINES", verts, decorator_color_special, edges) + + verts = [Vector((0, 0.75, 0)), Vector((-0.15, 0.6, 0)), Vector((0.15, 0.6, 0))] + edges = [[0, 1], [0, 2]] + verts = [angle @ v for v in verts] + self.line_shader.uniform_float("lineWidth", 5.0) + self.draw_batch("LINES", verts, decorator_color_special, edges) + self.line_shader.uniform_float("lineWidth", 2.0) + + arc_start = Vector((0, 0.25, 0)) + arc_end = angle @ arc_start + angle_half = Matrix.Rotation(radians(self.tn_angle / 2), 4, "Z") + arc_mid = angle_half @ arc_start + arc_segments = tool.Cad.create_arc_segments( + pts=[arc_start, arc_mid, arc_end], num_verts=12, make_edges=True + ) + verts, edges = arc_segments + self.draw_batch("LINES", verts, decorator_color_special, edges) + + if wcs_matrix: + if wcs_matrix.translation.length < 1000: + verts = [Vector((0, 0, 0)), wcs_matrix.translation] + edges = [[0, 1]] + self.draw_batch("LINES", verts, decorator_color_special, edges) + self.draw_batch("POINTS", verts[1:], decorator_color_special) + else: + edges = [[0, 1]] + verts = [Vector((0, 0, 0)), wcs_matrix.translation.normalized() * 3] + self.draw_batch("LINES", verts, decorator_color_special, edges) + verts = [wcs_matrix.translation.normalized() * 3, wcs_matrix.translation.normalized() * 3.1] + self.draw_batch("LINES", verts, decorator_color_error, edges) + verts = [wcs_matrix.translation.normalized() * 3.12, wcs_matrix.translation.normalized() * 3.2] + self.draw_batch("LINES", verts, decorator_color_error, edges) + verts = [wcs_matrix.translation.normalized() * 3.22, wcs_matrix.translation.normalized() * 3.3] + self.draw_batch("LINES", verts, decorator_color_error, edges) + verts = [wcs_matrix.translation.normalized() * 3.32, wcs_matrix.translation.normalized() * 3.4] + self.draw_batch("LINES", verts, decorator_color_error, edges) + + def calculate_angles(self, context): + self.pn_angle = 0.0 + self.gn_angle = float(GeoreferenceData.data["map_derived_angle"] or 0) + self.tn_angle = float(GeoreferenceData.data["true_derived_angle"] or 0) + + props = context.scene.BIMGeoreferenceProperties + if props.has_blender_offset: + blender_angle = ifcopenshell.util.geolocation.xaxis2angle( + float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate) + ) + self.pn_angle += blender_angle + self.gn_angle += blender_angle + self.tn_angle += blender_angle + self.pn_angle = tool.Cad.normalise_angle(self.pn_angle) + self.gn_angle = tool.Cad.normalise_angle(self.gn_angle) + self.tn_angle = tool.Cad.normalise_angle(self.tn_angle) diff --git a/src/blenderbim/blenderbim/bim/module/georeference/operator.py b/src/blenderbim/blenderbim/bim/module/georeference/operator.py index 808090c5c6..5a8a55c19a 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/operator.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/operator.py @@ -20,6 +20,7 @@ import bpy import blenderbim.tool as tool import blenderbim.core.georeference as core +from blenderbim.bim.module.georeference.decorator import GeoreferenceDecorator class AddGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/blenderbim/blenderbim/bim/module/georeference/prop.py b/src/blenderbim/blenderbim/bim/module/georeference/prop.py index 30486f2a43..dfa947e03c 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/prop.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/prop.py @@ -31,6 +31,7 @@ from bpy.props import ( CollectionProperty, ) from blenderbim.bim.module.georeference.data import GeoreferenceData +from blenderbim.bim.module.georeference.decorator import GeoreferenceDecorator def get_coordinate_operation_class(self, context): @@ -93,6 +94,13 @@ def update_grid_north_vector(self, context): self.is_changing_angle = False +def update_should_visualise(self, context): + if self.should_visualise: + GeoreferenceDecorator.install(bpy.context) + else: + GeoreferenceDecorator.uninstall() + + class BIMGeoreferenceProperties(PropertyGroup): coordinate_operation_class: bpy.props.EnumProperty( items=get_coordinate_operation_class, name="Coordinate Operation Class" @@ -109,6 +117,12 @@ class BIMGeoreferenceProperties(PropertyGroup): map_coordinates: StringProperty( name="Map Coordinates", description='Formatted "x,y,z" (without quotes)', default="0,0,0" ) + should_visualise: BoolProperty( + name="Should Visualise", + description="Displays a visualisation of all georeferencing data at the origin", + default=False, + update=update_should_visualise, + ) grid_north_angle: StringProperty(name="Grid North Angle", update=update_grid_north_angle) x_axis_abscissa: StringProperty(name="X Axis Abscissa", update=update_grid_north_vector) x_axis_ordinate: StringProperty(name="X Axis Ordinate", update=update_grid_north_vector) diff --git a/src/blenderbim/blenderbim/bim/module/georeference/ui.py b/src/blenderbim/blenderbim/bim/module/georeference/ui.py index 4b2a359cf0..5c85cc152a 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/ui.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/ui.py @@ -60,14 +60,20 @@ class BIM_PT_gis(Panel): if attribute.name == "XAxisAbscissa": row = self.layout.row(align=True) row.prop(props, "grid_north_angle", text="Angle") - row.prop(props, "x_axis_is_null", icon="RADIOBUT_OFF" if props.x_axis_is_null else "RADIOBUT_ON", text="") + row.prop( + props, "x_axis_is_null", icon="RADIOBUT_OFF" if props.x_axis_is_null else "RADIOBUT_ON", text="" + ) row = self.layout.row(align=True) row.prop(props, "x_axis_abscissa", text="XAxis Abscissa") - row.prop(props, "x_axis_is_null", icon="RADIOBUT_OFF" if props.x_axis_is_null else "RADIOBUT_ON", text="") + row.prop( + props, "x_axis_is_null", icon="RADIOBUT_OFF" if props.x_axis_is_null else "RADIOBUT_ON", text="" + ) elif attribute.name == "XAxisOrdinate": row = self.layout.row(align=True) row.prop(props, "x_axis_ordinate", text="XAxis Ordinate") - row.prop(props, "x_axis_is_null", icon="RADIOBUT_OFF" if props.x_axis_is_null else "RADIOBUT_ON", text="") + row.prop( + props, "x_axis_is_null", icon="RADIOBUT_OFF" if props.x_axis_is_null else "RADIOBUT_ON", text="" + ) if hasattr(context.scene, "sun_pos_properties"): row = self.layout.row(align=True) row.operator("bim.set_ifc_grid_north", text="Set IFC North") @@ -83,8 +89,9 @@ class BIM_PT_gis(Panel): row.label(text="IFC2X3 Fallback In Use", icon="INFO") if not GeoreferenceData.data["projected_crs"]: - row = self.layout.row() + row = self.layout.row(align=True) row.label(text="Not Georeferenced", icon="ERROR") + row.prop(props, "should_visualise", icon="HIDE_OFF", text="") if tool.Ifc.get_schema() != "IFC2X3": row = self.layout.row(align=True) row.prop(props, "coordinate_operation_class", text="") @@ -93,6 +100,7 @@ class BIM_PT_gis(Panel): if GeoreferenceData.data["projected_crs"]: row = self.layout.row(align=True) row.label(text="Projected CRS", icon="WORLD") + row.prop(props, "should_visualise", icon="HIDE_OFF", text="") if tool.Ifc.get_schema() != "IFC2X3": row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="") row.operator("bim.remove_georeferencing", icon="X", text="") @@ -195,9 +203,6 @@ class BIM_PT_gis_blender(Panel): props = context.scene.BIMGeoreferenceProperties - row = self.layout.row() - row.label(text="Blender Session Coordinates", icon="BLENDER") - if props.has_blender_offset: row = self.layout.row() row.label(text="Temporary Offset Is Active", icon="TRACKING_REFINE_FORWARDS") diff --git a/src/blenderbim/blenderbim/core/georeference.py b/src/blenderbim/blenderbim/core/georeference.py index b09302f5cb..b36445163e 100644 --- a/src/blenderbim/blenderbim/core/georeference.py +++ b/src/blenderbim/blenderbim/core/georeference.py @@ -99,6 +99,7 @@ def edit_wcs(ifc, georeference): wcs = georeference.export_wcs() georeference.set_wcs(wcs) georeference.disable_editing_wcs() + georeference.set_model_origin() def enable_editing_true_north(georeference):