From 95d2d0af831674212969f5a5d211747323d632c9 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 23 Jun 2024 22:23:39 +1000 Subject: [PATCH] You can now see and edit WCS in the Blender UI (new API for it too) --- .../bim/module/georeference/__init__.py | 25 +++--- .../bim/module/georeference/data.py | 21 ++++- .../bim/module/georeference/operator.py | 30 +++++++ .../bim/module/georeference/prop.py | 5 ++ .../blenderbim/bim/module/georeference/ui.py | 51 ++++++++++- .../blenderbim/core/georeference.py | 15 ++++ src/blenderbim/blenderbim/core/tool.py | 7 +- .../blenderbim/tool/georeference.py | 43 +++++++++- .../ifcopenshell/api/georeference/__init__.py | 2 + .../ifcopenshell/api/georeference/edit_wcs.py | 85 +++++++++++++++++++ .../test/api/georeference/test_edit_wcs.py | 47 ++++++++++ 11 files changed, 314 insertions(+), 17 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/georeference/edit_wcs.py create mode 100644 src/ifcopenshell-python/test/api/georeference/test_edit_wcs.py diff --git a/src/blenderbim/blenderbim/bim/module/georeference/__init__.py b/src/blenderbim/blenderbim/bim/module/georeference/__init__.py index bf145af93c..ad6a98f229 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/__init__.py @@ -20,20 +20,23 @@ import bpy from . import ui, prop, operator classes = ( - operator.EnableEditingGeoreferencing, - operator.DisableEditingGeoreferencing, - operator.EditGeoreferencing, - operator.SetIfcGridNorth, - operator.SetBlenderGridNorth, - operator.SetIfcTrueNorth, - operator.SetBlenderTrueNorth, - operator.RemoveGeoreferencing, operator.AddGeoreferencing, - operator.ConvertLocalToGlobal, - operator.ConvertGlobalToLocal, - operator.GetCursorLocation, operator.ConvertAngleToCoordinates, + operator.ConvertGlobalToLocal, + operator.ConvertLocalToGlobal, + operator.DisableEditingGeoreferencing, + operator.DisableEditingWCS, + operator.EditGeoreferencing, + operator.EditWCS, + operator.EnableEditingGeoreferencing, + operator.EnableEditingWCS, + operator.GetCursorLocation, operator.ImportPlot, + operator.RemoveGeoreferencing, + operator.SetBlenderGridNorth, + operator.SetBlenderTrueNorth, + operator.SetIfcGridNorth, + operator.SetIfcTrueNorth, prop.BIMGeoreferenceProperties, ui.BIM_PT_gis, ui.BIM_PT_gis_calculator, diff --git a/src/blenderbim/blenderbim/bim/module/georeference/data.py b/src/blenderbim/blenderbim/bim/module/georeference/data.py index 9efb0f912f..a13ee0625c 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/data.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/data.py @@ -18,8 +18,9 @@ import bpy -import ifcopenshell.util.geolocation +import numpy as np import blenderbim.tool as tool +import ifcopenshell.util.geolocation from ifcopenshell.util.doc import get_entity_doc @@ -42,6 +43,7 @@ class GeoreferenceData: cls.data["true_derived_angle"] = cls.true_derived_angle() cls.data["local_unit_symbol"] = cls.local_unit_symbol() cls.data["map_unit_symbol"] = cls.map_unit_symbol() + cls.data["world_coordinate_system"] = cls.world_coordinate_system() cls.is_loaded = True @classmethod @@ -143,7 +145,7 @@ class GeoreferenceData: @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)) + return str(round(ifcopenshell.util.geolocation.yaxis2angle(*cls.data["true_north"][:2]), 3)) @classmethod def local_unit_symbol(cls): @@ -161,3 +163,18 @@ class GeoreferenceData: return cls.local_unit_symbol() return ifcopenshell.util.unit.get_unit_symbol(unit) return "n/a" + + @classmethod + def world_coordinate_system(cls): + wcs = ifcopenshell.util.geolocation.get_wcs(tool.Ifc.get()) + result = {} + if wcs is None: + result["has_transformation"] = False + return result + if np.allclose(wcs, np.eye(4)): + result["has_transformation"] = False + else: + 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] + return result diff --git a/src/blenderbim/blenderbim/bim/module/georeference/operator.py b/src/blenderbim/blenderbim/bim/module/georeference/operator.py index 8596ec8d0a..0e9eb7528f 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/operator.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/operator.py @@ -189,3 +189,33 @@ class ImportPlot(bpy.types.Operator, tool.Ifc.Operator): def invoke(self, context, event): context.window_manager.fileselect_add(self) return {"RUNNING_MODAL"} + + +class EnableEditingWCS(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.enable_editing_wcs" + bl_label = "Enable Editing WCS" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Enable editing WCS" + + def _execute(self, context): + core.enable_editing_wcs(tool.Georeference) + + +class EditWCS(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.edit_wcs" + bl_label = "Edit WCS" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Edit the WCS" + + def _execute(self, context): + core.edit_wcs(tool.Ifc, tool.Georeference) + + +class DisableEditingWCS(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.disable_editing_wcs" + bl_label = "Disable Editing WCS" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Close editing panel" + + def _execute(self, context): + core.disable_editing_wcs(tool.Georeference) diff --git a/src/blenderbim/blenderbim/bim/module/georeference/prop.py b/src/blenderbim/blenderbim/bim/module/georeference/prop.py index add74e8ea5..c79f7937ee 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/prop.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/prop.py @@ -43,6 +43,7 @@ class BIMGeoreferenceProperties(PropertyGroup): items=get_coordinate_operation_class, name="Coordinate Operation Class" ) is_editing: BoolProperty(name="Is Editing") + is_editing_wcs: BoolProperty(name="Is Editing WCS") coordinate_operation: CollectionProperty(name="Coordinate Operation", type=Attribute) projected_crs: CollectionProperty(name="Projected CRS", type=Attribute) local_coordinates: StringProperty( @@ -83,3 +84,7 @@ class BIMGeoreferenceProperties(PropertyGroup): has_true_north: BoolProperty(name="Has True North", default=True) true_north_abscissa: StringProperty(name="True North Abscissa") true_north_ordinate: StringProperty(name="True North Ordinate") + wcs_x: StringProperty(name="WCS X", default="0") + wcs_y: StringProperty(name="WCS Y", default="0") + wcs_z: StringProperty(name="WCS Z", default="0") + wcs_rotation: StringProperty(name="WCS Rotation", default="0") diff --git a/src/blenderbim/blenderbim/bim/module/georeference/ui.py b/src/blenderbim/blenderbim/bim/module/georeference/ui.py index e81cf9015f..bff8b4ee19 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/ui.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/ui.py @@ -40,8 +40,14 @@ class BIM_PT_gis(Panel): GeoreferenceData.load() if props.is_editing: - return self.draw_editable_ui(context) - self.draw_ui(context) + self.draw_editable_ui(context) + else: + self.draw_ui(context) + + if props.is_editing_wcs: + self.draw_editable_wcs_ui(context) + else: + self.draw_wcs_ui() def draw_editable_ui(self, context): props = context.scene.BIMGeoreferenceProperties @@ -157,6 +163,47 @@ class BIM_PT_gis(Panel): row.label(text="Derived Angle") row.label(text=GeoreferenceData.data["true_derived_angle"]) + def draw_wcs_ui(self): + row = self.layout.row(align=True) + row.label(text="World Coordinate System", icon="EMPTY_ARROWS") + row.operator("bim.enable_editing_wcs", icon="GREASEPENCIL", text="") + + if GeoreferenceData.data["world_coordinate_system"]["has_transformation"]: + row = self.layout.row() + row.label(text="Unrecommended Transformation Found", icon="ERROR") + row = self.layout.row(align=True) + row.label(text="X") + row.label(text=str(GeoreferenceData.data["world_coordinate_system"]["x"])) + row = self.layout.row(align=True) + row.label(text="Y") + row.label(text=str(GeoreferenceData.data["world_coordinate_system"]["y"])) + row = self.layout.row(align=True) + row.label(text="Z") + row.label(text=str(GeoreferenceData.data["world_coordinate_system"]["z"])) + row = self.layout.row(align=True) + row.label(text="Rotation") + row.label(text=str(GeoreferenceData.data["world_coordinate_system"]["rotation"])) + else: + row = self.layout.row() + row.label(text="No WCS Transformation", icon="CHECKMARK") + + def draw_editable_wcs_ui(self, context): + props = context.scene.BIMGeoreferenceProperties + + row = self.layout.row(align=True) + row.label(text="World Coordinate System", icon="EMPTY_ARROWS") + row.operator("bim.edit_wcs", icon="CHECKMARK", text="") + row.operator("bim.disable_editing_wcs", icon="CANCEL", text="") + + row = self.layout.row() + row.prop(props, "wcs_x") + row = self.layout.row() + row.prop(props, "wcs_y") + row = self.layout.row() + row.prop(props, "wcs_z") + row = self.layout.row() + row.prop(props, "wcs_rotation") + class BIM_PT_gis_calculator(Panel): bl_idname = "BIM_PT_gis_calculator" diff --git a/src/blenderbim/blenderbim/core/georeference.py b/src/blenderbim/blenderbim/core/georeference.py index 22035b9dda..485cff511d 100644 --- a/src/blenderbim/blenderbim/core/georeference.py +++ b/src/blenderbim/blenderbim/core/georeference.py @@ -85,3 +85,18 @@ def convert_angle_to_coord(georeference, type): def import_plot(georeference, filepath): georeference.import_plot(filepath) + + +def enable_editing_wcs(georeference): + georeference.import_wcs() + georeference.enable_editing_wcs() + + +def disable_editing_wcs(georeference): + georeference.disable_editing_wcs() + + +def edit_wcs(ifc, georeference): + wcs = georeference.export_wcs() + georeference.set_wcs(wcs) + georeference.disable_editing_wcs() diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 8e7f6bef64..cb7251975f 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -418,17 +418,21 @@ class Geometry: class Georeference: def angle2coords(cls, angle, type): pass def disable_editing(cls): pass + def disable_editing_wcs(cls): pass def enable_editing(cls): pass + def enable_editing_wcs(cls): pass def enh2xyz(cls, coordinates): pass + def export_wcs(cls): pass def get_angle(cls, type): pass + def get_coordinate_operation_attributes(cls): pass def get_coordinates(cls, io): pass def get_cursor_location(cls): pass - def get_coordinate_operation_attributes(cls): pass def get_projected_crs_attributes(cls): pass def get_true_north_attributes(cls): pass def import_map_conversion(cls): pass def import_projected_crs(cls): pass def import_true_north(cls): pass + def import_wcs(cls): pass def set_blender_grid_north(cls): pass def set_blender_true_north(cls): pass def set_coordinates(cls, io, coordinates): pass @@ -436,6 +440,7 @@ class Georeference: def set_ifc_grid_north(cls): pass def set_ifc_true_north(cls): pass def set_vector_coordinates(cls, vector_coordinates, type): pass + def set_wcs(cls, matrix): pass def xyz2enh(cls, coordinates): pass diff --git a/src/blenderbim/blenderbim/tool/georeference.py b/src/blenderbim/blenderbim/tool/georeference.py index 2578134611..5a3c87270e 100644 --- a/src/blenderbim/blenderbim/tool/georeference.py +++ b/src/blenderbim/blenderbim/tool/georeference.py @@ -18,9 +18,11 @@ import bpy import json +import numpy as np +import ifcopenshell +import ifcopenshell.api.georeference import blenderbim.core.tool import blenderbim.tool as tool -import ifcopenshell import blenderbim.bim.helper from math import radians, cos, sin @@ -169,6 +171,14 @@ class Georeference(blenderbim.core.tool.Georeference): def disable_editing(cls): bpy.context.scene.BIMGeoreferenceProperties.is_editing = False + @classmethod + def enable_editing_wcs(cls): + bpy.context.scene.BIMGeoreferenceProperties.is_editing_wcs = True + + @classmethod + def disable_editing_wcs(cls): + bpy.context.scene.BIMGeoreferenceProperties.is_editing_wcs = False + @classmethod def set_coordinates(cls, io, coordinates): if io == "local": @@ -315,3 +325,34 @@ class Georeference(blenderbim.core.tool.Georeference): bm.verts.new(vertex) bm.to_mesh(mesh) bm.free() + + @classmethod + def import_wcs(cls): + props = bpy.context.scene.BIMGeoreferenceProperties + wcs = None + for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): + wcs = context.WorldCoordinateSystem + if context.ContextType == "Model": + break + if not wcs: + return + placement = ifcopenshell.util.placement.get_axis2placement(wcs) + if np.allclose(placement, np.eye(4)): + props.wcs_x = props.wcs_y = props.wcs_z = props.wcs_rotation = "0" + else: + props.wcs_rotation = str(round(ifcopenshell.util.geolocation.yaxis2angle(*placement[:, 1][:2]), 3)) + props.wcs_x, props.wcs_y, props.wcs_z = map(str, placement[:, 3][:3]) + + @classmethod + def export_wcs(cls): + props = bpy.context.scene.BIMGeoreferenceProperties + return { + "x": float(props.wcs_x), + "y": float(props.wcs_y), + "z": float(props.wcs_z), + "rotation": float(props.wcs_rotation), + } + + @classmethod + def set_wcs(cls, wcs): + ifcopenshell.api.georeference.edit_wcs(tool.Ifc.get(), **wcs, is_si=False) diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/__init__.py index 49f272b6e0..986c1d3b1b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/__init__.py @@ -26,6 +26,7 @@ map coordinates and project local engineering coordinates. from .. import wrap_usecases from .add_georeferencing import add_georeferencing from .edit_georeferencing import edit_georeferencing +from .edit_wcs import edit_wcs from .remove_georeferencing import remove_georeferencing wrap_usecases(__path__, __name__) @@ -33,5 +34,6 @@ wrap_usecases(__path__, __name__) __all__ = [ "add_georeferencing", "edit_georeferencing", + "edit_wcs", "remove_georeferencing", ] diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_wcs.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_wcs.py new file mode 100644 index 0000000000..4ec4bf96e1 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_wcs.py @@ -0,0 +1,85 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import ifcopenshell +import ifcopenshell.util.element +import ifcopenshell.util.geolocation +import numpy as np +from math import sin, cos, radians + + +def edit_wcs( + file: ifcopenshell.file, + x: float = 0.0, + y: float = 0.0, + z: float = 0.0, + rotation: float = 0.0, + is_si: bool = True, +) -> None: + """Edits the WCS for all geometric contexts to a translation and rotation + + It's recommended to leave the WCS at 0,0,0. You should generally not be + setting it to any value. Instead, your project's origin should use a + coordinate operation to convert to the projected CRS. + + :param x: The X translation of the WCS + :param y: The Y translation of the WCS + :param z: The Z translation of the WCS + :param rotation: The rotation around the Z axis (i.e. top down plan view) + in decimal degrees of the WCS. Anticlockwise is positive. + + Example: + + .. code:: python + + # This is the simplest scenario, resetting the WCS to 0,0,0 with no rotation (recommended) + ifcopenshell.api.run("georeference.edit_wcs", model) + """ + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) + if np.isclose(rotation, 0): + xaxis_x = 1.0 + xaxis_y = 0.0 + else: + xaxis_x = cos(radians(rotation)) + xaxis_y = sin(radians(rotation)) + if np.allclose((x, y, z), (0, 0, 0)): + x = y = z = 0.0 + for context in file.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + old_wcs = context.WorldCoordinateSystem + if context.CoordinateSpaceDimension == 3: + if is_si: + point = file.createIfcCartesianPoint((x / unit_scale, y / unit_scale, z / unit_scale)) + else: + point = file.createIfcCartesianPoint((x, y, z)) + placement = file.createIfcAxis2Placement3D( + point, + file.createIfcDirection((0.0, 0.0, 1.0)), + file.createIfcDirection((xaxis_x, xaxis_y, 0.0)), + ) + elif context.CoordinateSpaceDimension == 2: + if is_si: + point = file.createIfcCartesianPoint((x / unit_scale, y / unit_scale)) + else: + point = file.createIfcCartesianPoint((x, y)) + placement = file.createIfcAxis2Placement2D( + point, + file.createIfcDirection((xaxis_x, xaxis_y)), + ) + context.WorldCoordinateSystem = placement + if file.get_total_inverses(old_wcs) == 0: + ifcopenshell.util.element.remove_deep2(file, old_wcs) diff --git a/src/ifcopenshell-python/test/api/georeference/test_edit_wcs.py b/src/ifcopenshell-python/test/api/georeference/test_edit_wcs.py new file mode 100644 index 0000000000..f23ce928ce --- /dev/null +++ b/src/ifcopenshell-python/test/api/georeference/test_edit_wcs.py @@ -0,0 +1,47 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import numpy as np +import test.bootstrap +import ifcopenshell.api.root +import ifcopenshell.api.context +import ifcopenshell.api.georeference +import ifcopenshell.util.geolocation + + +class TestEditWCS(test.bootstrap.IFC4): + def test_editing_wcs(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + ifcopenshell.api.context.add_context(self.file, "Model") + ifcopenshell.api.context.add_context(self.file, "Plan") + + ifcopenshell.api.georeference.edit_wcs(self.file) + wcs = ifcopenshell.util.geolocation.get_wcs(self.file) + m = np.eye(4) + assert np.allclose(wcs, m) + + ifcopenshell.api.georeference.edit_wcs(self.file, x=1, y=2, z=3) + m[:, 3] = [1, 2, 3, 1] + wcs = ifcopenshell.util.geolocation.get_wcs(self.file) + assert np.allclose(wcs, m) + + ifcopenshell.api.georeference.edit_wcs(self.file, x=1, y=2, z=3, rotation=90) + m[:, 0] = [0, 1, 0, 0] + m[:, 1] = [-1, 0, 0, 0] + wcs = ifcopenshell.util.geolocation.get_wcs(self.file) + assert np.allclose(wcs, m)