You can now see and edit WCS in the Blender UI (new API for it too)

This commit is contained in:
Dion Moult
2024-06-23 22:23:39 +10:00
parent 03ee826e67
commit 95d2d0af83
11 changed files with 314 additions and 17 deletions
@@ -20,20 +20,23 @@ import bpy
from . import ui, prop, operator from . import ui, prop, operator
classes = ( classes = (
operator.EnableEditingGeoreferencing,
operator.DisableEditingGeoreferencing,
operator.EditGeoreferencing,
operator.SetIfcGridNorth,
operator.SetBlenderGridNorth,
operator.SetIfcTrueNorth,
operator.SetBlenderTrueNorth,
operator.RemoveGeoreferencing,
operator.AddGeoreferencing, operator.AddGeoreferencing,
operator.ConvertLocalToGlobal,
operator.ConvertGlobalToLocal,
operator.GetCursorLocation,
operator.ConvertAngleToCoordinates, operator.ConvertAngleToCoordinates,
operator.ConvertGlobalToLocal,
operator.ConvertLocalToGlobal,
operator.DisableEditingGeoreferencing,
operator.DisableEditingWCS,
operator.EditGeoreferencing,
operator.EditWCS,
operator.EnableEditingGeoreferencing,
operator.EnableEditingWCS,
operator.GetCursorLocation,
operator.ImportPlot, operator.ImportPlot,
operator.RemoveGeoreferencing,
operator.SetBlenderGridNorth,
operator.SetBlenderTrueNorth,
operator.SetIfcGridNorth,
operator.SetIfcTrueNorth,
prop.BIMGeoreferenceProperties, prop.BIMGeoreferenceProperties,
ui.BIM_PT_gis, ui.BIM_PT_gis,
ui.BIM_PT_gis_calculator, ui.BIM_PT_gis_calculator,
@@ -18,8 +18,9 @@
import bpy import bpy
import ifcopenshell.util.geolocation import numpy as np
import blenderbim.tool as tool import blenderbim.tool as tool
import ifcopenshell.util.geolocation
from ifcopenshell.util.doc import get_entity_doc 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["true_derived_angle"] = cls.true_derived_angle()
cls.data["local_unit_symbol"] = cls.local_unit_symbol() cls.data["local_unit_symbol"] = cls.local_unit_symbol()
cls.data["map_unit_symbol"] = cls.map_unit_symbol() cls.data["map_unit_symbol"] = cls.map_unit_symbol()
cls.data["world_coordinate_system"] = cls.world_coordinate_system()
cls.is_loaded = True cls.is_loaded = True
@classmethod @classmethod
@@ -143,7 +145,7 @@ class GeoreferenceData:
@classmethod @classmethod
def true_derived_angle(cls): def true_derived_angle(cls):
if cls.data["true_north"]: 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 @classmethod
def local_unit_symbol(cls): def local_unit_symbol(cls):
@@ -161,3 +163,18 @@ class GeoreferenceData:
return cls.local_unit_symbol() return cls.local_unit_symbol()
return ifcopenshell.util.unit.get_unit_symbol(unit) return ifcopenshell.util.unit.get_unit_symbol(unit)
return "n/a" 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
@@ -189,3 +189,33 @@ class ImportPlot(bpy.types.Operator, tool.Ifc.Operator):
def invoke(self, context, event): def invoke(self, context, event):
context.window_manager.fileselect_add(self) context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"} 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)
@@ -43,6 +43,7 @@ class BIMGeoreferenceProperties(PropertyGroup):
items=get_coordinate_operation_class, name="Coordinate Operation Class" items=get_coordinate_operation_class, name="Coordinate Operation Class"
) )
is_editing: BoolProperty(name="Is Editing") is_editing: BoolProperty(name="Is Editing")
is_editing_wcs: BoolProperty(name="Is Editing WCS")
coordinate_operation: CollectionProperty(name="Coordinate Operation", type=Attribute) coordinate_operation: CollectionProperty(name="Coordinate Operation", type=Attribute)
projected_crs: CollectionProperty(name="Projected CRS", type=Attribute) projected_crs: CollectionProperty(name="Projected CRS", type=Attribute)
local_coordinates: StringProperty( local_coordinates: StringProperty(
@@ -83,3 +84,7 @@ class BIMGeoreferenceProperties(PropertyGroup):
has_true_north: BoolProperty(name="Has True North", default=True) has_true_north: BoolProperty(name="Has True North", default=True)
true_north_abscissa: StringProperty(name="True North Abscissa") true_north_abscissa: StringProperty(name="True North Abscissa")
true_north_ordinate: StringProperty(name="True North Ordinate") 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")
@@ -40,8 +40,14 @@ class BIM_PT_gis(Panel):
GeoreferenceData.load() GeoreferenceData.load()
if props.is_editing: if props.is_editing:
return self.draw_editable_ui(context) self.draw_editable_ui(context)
self.draw_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): def draw_editable_ui(self, context):
props = context.scene.BIMGeoreferenceProperties props = context.scene.BIMGeoreferenceProperties
@@ -157,6 +163,47 @@ class BIM_PT_gis(Panel):
row.label(text="Derived Angle") row.label(text="Derived Angle")
row.label(text=GeoreferenceData.data["true_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): class BIM_PT_gis_calculator(Panel):
bl_idname = "BIM_PT_gis_calculator" bl_idname = "BIM_PT_gis_calculator"
@@ -85,3 +85,18 @@ def convert_angle_to_coord(georeference, type):
def import_plot(georeference, filepath): def import_plot(georeference, filepath):
georeference.import_plot(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()
+6 -1
View File
@@ -418,17 +418,21 @@ class Geometry:
class Georeference: class Georeference:
def angle2coords(cls, angle, type): pass def angle2coords(cls, angle, type): pass
def disable_editing(cls): pass def disable_editing(cls): pass
def disable_editing_wcs(cls): pass
def enable_editing(cls): pass def enable_editing(cls): pass
def enable_editing_wcs(cls): pass
def enh2xyz(cls, coordinates): pass def enh2xyz(cls, coordinates): pass
def export_wcs(cls): pass
def get_angle(cls, type): pass def get_angle(cls, type): pass
def get_coordinate_operation_attributes(cls): pass
def get_coordinates(cls, io): pass def get_coordinates(cls, io): pass
def get_cursor_location(cls): pass def get_cursor_location(cls): pass
def get_coordinate_operation_attributes(cls): pass
def get_projected_crs_attributes(cls): pass def get_projected_crs_attributes(cls): pass
def get_true_north_attributes(cls): pass def get_true_north_attributes(cls): pass
def import_map_conversion(cls): pass def import_map_conversion(cls): pass
def import_projected_crs(cls): pass def import_projected_crs(cls): pass
def import_true_north(cls): pass def import_true_north(cls): pass
def import_wcs(cls): pass
def set_blender_grid_north(cls): pass def set_blender_grid_north(cls): pass
def set_blender_true_north(cls): pass def set_blender_true_north(cls): pass
def set_coordinates(cls, io, coordinates): pass def set_coordinates(cls, io, coordinates): pass
@@ -436,6 +440,7 @@ class Georeference:
def set_ifc_grid_north(cls): pass def set_ifc_grid_north(cls): pass
def set_ifc_true_north(cls): pass def set_ifc_true_north(cls): pass
def set_vector_coordinates(cls, vector_coordinates, type): pass def set_vector_coordinates(cls, vector_coordinates, type): pass
def set_wcs(cls, matrix): pass
def xyz2enh(cls, coordinates): pass def xyz2enh(cls, coordinates): pass
+42 -1
View File
@@ -18,9 +18,11 @@
import bpy import bpy
import json import json
import numpy as np
import ifcopenshell
import ifcopenshell.api.georeference
import blenderbim.core.tool import blenderbim.core.tool
import blenderbim.tool as tool import blenderbim.tool as tool
import ifcopenshell
import blenderbim.bim.helper import blenderbim.bim.helper
from math import radians, cos, sin from math import radians, cos, sin
@@ -169,6 +171,14 @@ class Georeference(blenderbim.core.tool.Georeference):
def disable_editing(cls): def disable_editing(cls):
bpy.context.scene.BIMGeoreferenceProperties.is_editing = False 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 @classmethod
def set_coordinates(cls, io, coordinates): def set_coordinates(cls, io, coordinates):
if io == "local": if io == "local":
@@ -315,3 +325,34 @@ class Georeference(blenderbim.core.tool.Georeference):
bm.verts.new(vertex) bm.verts.new(vertex)
bm.to_mesh(mesh) bm.to_mesh(mesh)
bm.free() 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)
@@ -26,6 +26,7 @@ map coordinates and project local engineering coordinates.
from .. import wrap_usecases from .. import wrap_usecases
from .add_georeferencing import add_georeferencing from .add_georeferencing import add_georeferencing
from .edit_georeferencing import edit_georeferencing from .edit_georeferencing import edit_georeferencing
from .edit_wcs import edit_wcs
from .remove_georeferencing import remove_georeferencing from .remove_georeferencing import remove_georeferencing
wrap_usecases(__path__, __name__) wrap_usecases(__path__, __name__)
@@ -33,5 +34,6 @@ wrap_usecases(__path__, __name__)
__all__ = [ __all__ = [
"add_georeferencing", "add_georeferencing",
"edit_georeferencing", "edit_georeferencing",
"edit_wcs",
"remove_georeferencing", "remove_georeferencing",
] ]
@@ -0,0 +1,85 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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)
@@ -0,0 +1,47 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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)