Split true north in API and Blender UI to be editable independent of georeferencing

They are two separate concepts, after all.
This commit is contained in:
Dion Moult
2024-06-23 23:19:27 +10:00
parent 013b138e5c
commit e295d596fe
11 changed files with 273 additions and 110 deletions
@@ -25,10 +25,13 @@ classes = (
operator.ConvertGlobalToLocal,
operator.ConvertLocalToGlobal,
operator.DisableEditingGeoreferencing,
operator.DisableEditingTrueNorth,
operator.DisableEditingWCS,
operator.EditGeoreferencing,
operator.EditTrueNorth,
operator.EditWCS,
operator.EnableEditingGeoreferencing,
operator.EnableEditingTrueNorth,
operator.EnableEditingWCS,
operator.GetCursorLocation,
operator.ImportPlot,
@@ -39,6 +42,8 @@ classes = (
operator.SetIfcTrueNorth,
prop.BIMGeoreferenceProperties,
ui.BIM_PT_gis,
ui.BIM_PT_gis_wcs,
ui.BIM_PT_gis_true_north,
ui.BIM_PT_gis_calculator,
)
@@ -219,3 +219,33 @@ class DisableEditingWCS(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.disable_editing_wcs(tool.Georeference)
class EnableEditingTrueNorth(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_true_north"
bl_label = "Enable Editing True North"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Enable editing True North"
def _execute(self, context):
core.enable_editing_true_north(tool.Georeference)
class EditTrueNorth(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_true_north"
bl_label = "Edit True North"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Edit the True North"
def _execute(self, context):
core.edit_true_north(tool.Ifc, tool.Georeference)
class DisableEditingTrueNorth(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_true_north"
bl_label = "Disable Editing True North"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Close editing panel"
def _execute(self, context):
core.disable_editing_true_north(tool.Georeference)
@@ -44,6 +44,7 @@ class BIMGeoreferenceProperties(PropertyGroup):
)
is_editing: BoolProperty(name="Is Editing")
is_editing_wcs: BoolProperty(name="Is Editing WCS")
is_editing_true_north: BoolProperty(name="Is Editing True North")
coordinate_operation: CollectionProperty(name="Coordinate Operation", type=Attribute)
projected_crs: CollectionProperty(name="Projected CRS", type=Attribute)
local_coordinates: StringProperty(
@@ -44,11 +44,6 @@ class BIM_PT_gis(Panel):
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
row = self.layout.row(align=True)
@@ -68,19 +63,6 @@ class BIM_PT_gis(Panel):
row.operator("bim.set_blender_grid_north", text="Set Blender North")
draw_attribute(attribute, self.layout.row())
row = self.layout.row()
row.label(text="True North", icon="LIGHT_SUN")
row = self.layout.row()
row.prop(props, "has_true_north")
row = self.layout.row()
row.prop(props, "true_north_abscissa")
row = self.layout.row()
row.prop(props, "true_north_ordinate")
if hasattr(context.scene, "sun_pos_properties"):
row = self.layout.row(align=True)
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
@@ -153,24 +135,83 @@ class BIM_PT_gis(Panel):
row.label(text="Derived Angle")
row.label(text=GeoreferenceData.data["map_derived_angle"])
class BIM_PT_gis_true_north(Panel):
bl_idname = "BIM_PT_gis_true_north"
bl_label = "True North"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_gis"
def draw(self, context):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
self.props = context.scene.BIMGeoreferenceProperties
if self.props.is_editing_true_north:
self.draw_editable_ui(context)
else:
self.draw_ui()
def draw_editable_ui(self, context):
row = self.layout.row()
row.prop(self.props, "has_true_north")
row = self.layout.row()
row.prop(self.props, "true_north_abscissa")
row = self.layout.row()
row.prop(self.props, "true_north_ordinate")
if hasattr(context.scene, "sun_pos_properties"):
row = self.layout.row(align=True)
row.operator("bim.set_ifc_true_north", text="Set IFC North")
row.operator("bim.set_blender_true_north", text="Set Blender North")
row = self.layout.row(align=True)
row.operator("bim.edit_true_north", icon="CHECKMARK")
row.operator("bim.disable_editing_true_north", icon="CANCEL", text="")
def draw_ui(self):
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(GeoreferenceData.data["true_north"][0:2])[1:-1])
row.operator("bim.enable_editing_true_north", icon="GREASEPENCIL", text="")
row = self.layout.row(align=True)
row.label(text="Derived Angle")
row.label(text=GeoreferenceData.data["true_derived_angle"])
else:
row = self.layout.row(align=True)
row.label(text="No True North Found", icon="LIGHT_SUN")
row.operator("bim.enable_editing_true_north", icon="GREASEPENCIL", text="")
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="")
class BIM_PT_gis_wcs(Panel):
bl_idname = "BIM_PT_gis_wcs"
bl_label = "World Coordinate System"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_gis"
def draw(self, context):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
props = context.scene.BIMGeoreferenceProperties
if props.is_editing_wcs:
self.draw_editable_ui(context)
else:
self.draw_ui()
def draw_ui(self):
if GeoreferenceData.data["world_coordinate_system"]["has_transformation"]:
row = self.layout.row()
row = self.layout.row(align=True)
row.label(text="Unrecommended Transformation Found", icon="ERROR")
row.operator("bim.enable_editing_wcs", icon="GREASEPENCIL", text="")
row = self.layout.row(align=True)
row.label(text="X")
row.label(text=str(GeoreferenceData.data["world_coordinate_system"]["x"]))
@@ -186,8 +227,9 @@ class BIM_PT_gis(Panel):
else:
row = self.layout.row()
row.label(text="No WCS Transformation", icon="CHECKMARK")
row.operator("bim.enable_editing_wcs", icon="GREASEPENCIL", text="")
def draw_editable_wcs_ui(self, context):
def draw_editable_ui(self, context):
props = context.scene.BIMGeoreferenceProperties
row = self.layout.row(align=True)
+14 -1
View File
@@ -24,7 +24,6 @@ def add_georeferencing(georeference):
def enable_editing_georeferencing(georeference):
georeference.import_projected_crs()
georeference.import_coordinate_operation()
georeference.import_true_north()
georeference.enable_editing()
@@ -100,3 +99,17 @@ def edit_wcs(ifc, georeference):
wcs = georeference.export_wcs()
georeference.set_wcs(wcs)
georeference.disable_editing_wcs()
def enable_editing_true_north(georeference):
georeference.import_true_north()
georeference.enable_editing_true_north()
def disable_editing_true_north(georeference):
georeference.disable_editing_true_north()
def edit_true_north(ifc, georeference):
ifc.run("georeference.edit_true_north", true_north=georeference.get_true_north_attributes())
georeference.disable_editing_true_north()
@@ -179,6 +179,15 @@ class Georeference(blenderbim.core.tool.Georeference):
def disable_editing_wcs(cls):
bpy.context.scene.BIMGeoreferenceProperties.is_editing_wcs = False
@classmethod
def enable_editing_true_north(cls):
bpy.context.scene.BIMGeoreferenceProperties.is_editing_true_north = True
@classmethod
def disable_editing_true_north(cls):
bpy.context.scene.BIMGeoreferenceProperties.is_editing_true_north = False
@classmethod
def set_coordinates(cls, io, coordinates):
if io == "local":
@@ -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_true_north import edit_true_north
from .edit_wcs import edit_wcs
from .remove_georeferencing import remove_georeferencing
@@ -34,6 +35,7 @@ wrap_usecases(__path__, __name__)
__all__ = [
"add_georeferencing",
"edit_georeferencing",
"edit_true_north",
"edit_wcs",
"remove_georeferencing",
]
@@ -24,7 +24,6 @@ def edit_georeferencing(
file: ifcopenshell.file,
coordinate_operation: Optional[dict[str, Any]] = None,
projected_crs: Optional[dict[str, Any]] = None,
true_north: Optional[tuple[float, float]] = None,
) -> None:
"""Edits the attributes of a map conversion, projected CRS, and true north
@@ -36,28 +35,18 @@ def edit_georeferencing(
https://docs.blenderbim.org/users/georeferencing.html
For more information about the attributes and data types of an
IfcMapConversion, consult the IFC documentation.
IfcCoordinateOperation, consult the IFC documentation.
For more information about the attributes and data types of an
IfcProjectedCRS, consult the IFC documentation.
True north is defined as a unitised 2D vector pointing to true north.
Note that true north is not part of georeferencing, and is only
optionally provided as a reference value, typically for solar analysis.
See ifcopenshell.util.geolocation for more utilities to convert to and
from local and map coordinates to check your results.
:param coordinate_operation: The dictionary of attribute names and values
you want to edit.
:type coordinate_operation: dict, optional
:param projected_crs: The IfcProjectedCRS dictionary of attribute
names and values you want to edit.
:type projected_crs: dict, optional
:param true_north: A unitised 2D vector, where each ordinate is a float
:type true_north: tuple[float, float], optional
:return: None
:rtype: None
Example:
@@ -88,64 +77,32 @@ def edit_georeferencing(
"Scale": 0.99956, # Ask your surveyor for your site's average combined scale factor!
})
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"coordinate_operation": coordinate_operation or {},
"projected_crs": projected_crs or {},
"true_north": true_north or [],
}
return usecase.execute()
class Usecase:
def execute(self):
self.set_true_north()
if self.file.schema == "IFC2X3":
if not (project := self.file.by_type("IfcProject")):
return
project = project[0]
if (crs := ifcopenshell.util.element.get_pset(project, "ePSet_ProjectedCRS")):
crs = self.file.by_id(crs["id"])
for k, v in self.settings["projected_crs"].items():
if k == "Description":
v = self.file.createIfcText(v)
elif k == "Name":
v = self.file.createIfcLabel(v)
else:
v = self.file.createIfcIdentifier(v)
ifcopenshell.api.pset.edit_pset(self.file, crs, properties=self.settings["projected_crs"])
if (conversion := ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion")):
conversion = self.file.by_id(conversion["id"])
for k, v in self.settings["coordinate_operation"].items():
if k in ("XAxisAbscissa", "XAxisOrdinate", "Scale"):
v = self.file.createIfcReal(v)
else:
v = self.file.createIfcLengthMeasure(v)
ifcopenshell.api.pset.edit_pset(self.file, conversion, properties=self.settings["coordinate_operation"])
if file.schema == "IFC2X3":
if not (project := file.by_type("IfcProject")):
return
coordinate_operation = self.file.by_type("IfcCoordinateOperation")[0]
projected_crs = self.file.by_type("IfcProjectedCRS")[0]
for name, value in self.settings["coordinate_operation"].items():
setattr(coordinate_operation, name, value)
for name, value in self.settings["projected_crs"].items():
setattr(projected_crs, name, value)
def set_true_north(self):
if not self.settings["true_north"]:
return
for context in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.TrueNorth:
if len(self.file.get_inverse(context.TrueNorth)) != 1:
context.TrueNorth = self.file.create_entity("IfcDirection")
else:
context.TrueNorth = self.file.create_entity("IfcDirection")
direction = context.TrueNorth
if self.settings["true_north"] is None:
# TODO: code will never be executed since None value
# is substituted by an empty list
context.TrueNorth = self.settings["true_north"]
elif context.CoordinateSpaceDimension == 2:
direction.DirectionRatios = self.settings["true_north"][0:2]
else:
direction.DirectionRatios = self.settings["true_north"][0:2] + [0.0]
project = project[0]
if crs := ifcopenshell.util.element.get_pset(project, "ePSet_ProjectedCRS"):
crs = file.by_id(crs["id"])
for k, v in projected_crs.items():
if k == "Description":
v = file.createIfcText(v)
elif k == "Name":
v = file.createIfcLabel(v)
else:
v = file.createIfcIdentifier(v)
ifcopenshell.api.pset.edit_pset(file, crs, properties=projected_crs)
if conversion := ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion"):
conversion = file.by_id(conversion["id"])
for k, v in coordinate_operation.items():
if k in ("XAxisAbscissa", "XAxisOrdinate", "Scale"):
v = file.createIfcReal(v)
else:
v = file.createIfcLengthMeasure(v)
ifcopenshell.api.pset.edit_pset(file, conversion, properties=coordinate_operation)
return
conversion = file.by_type("IfcCoordinateOperation")[0]
crs = file.by_type("IfcProjectedCRS")[0]
for name, value in coordinate_operation.items():
setattr(conversion, name, value)
for name, value in projected_crs.items():
setattr(crs, name, value)
@@ -0,0 +1,75 @@
# 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.geolocation
from typing import Union, Optional
def edit_true_north(file: ifcopenshell.file, true_north: Optional[Union[tuple[float, float], float]] = 0.0) -> None:
"""Edits the true north
Given project north being up (i.e. a vector of 0, 1), true north is defined
as a unitised 2D vector pointing to true north. Alternatively, true north
may be defined as a rotation from project north to true north.
Anticlockwise is positive.
Note that true north is not part of georeferencing, and is only optionally
provided as a reference value, typically for solar analysis. Remember: grid
north (what your surveyor will typically use) is not the same as true
north!
:param true_north: A unitised 2D vector, where each ordinate is a float, or
an angle in decimal degrees where anticlockwise is positive.
Example:
.. code:: python
# Both of these are identical, and indicate that:
# - If project north is up the page, true north is in the top left
# - The building is therefore facing north east
ifcopenshell.api.run("georeference.edit_true_north", model, true_north=30)
ifcopenshell.api.run("georeference.edit_true_north", model, true_north=(-0.5, 0.8660254))
# This unsets true north
ifcopenshell.api.run("georeference.edit_true_north", model, true_north=None)
"""
if not true_north:
return
if true_north is None:
pass
elif isinstance(true_north, (float, int)):
x, y = ifcopenshell.util.geolocation.angle2yaxis(true_north)
else:
x, y = true_north
for context in file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.TrueNorth:
if file.get_total_inverses(context.TrueNorth) != 1:
context.TrueNorth = file.create_entity("IfcDirection")
else:
context.TrueNorth = file.create_entity("IfcDirection")
direction = context.TrueNorth
if true_north is None:
context.TrueNorth = None
elif context.CoordinateSpaceDimension == 2:
direction.DirectionRatios = (x, y)
else:
direction.DirectionRatios = (x, y, 0.0)
@@ -31,7 +31,7 @@ class TestEditGeoreferencing(test.bootstrap.IFC4):
ifcopenshell.api.georeference.edit_georeferencing(
self.file,
projected_crs={"Name": "EPSG:7856"},
map_conversion={"Eastings": 123.45, "Northings": 234.56},
coordinate_operation={"Eastings": 123.45, "Northings": 234.56},
)
crs = self.file.by_type("IfcProjectedCRS")[0]
assert crs.Name == "EPSG:7856"
@@ -39,15 +39,6 @@ class TestEditGeoreferencing(test.bootstrap.IFC4):
assert conversion.Eastings == 123.45
assert conversion.Northings == 234.56
def test_editing_true_north(self):
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
model = ifcopenshell.api.context.add_context(self.file, "Model")
plan = ifcopenshell.api.context.add_context(self.file, "Plan")
ifcopenshell.api.georeference.add_georeferencing(self.file)
ifcopenshell.api.georeference.edit_georeferencing(self.file, true_north=[0.0, 1.0])
assert model.TrueNorth[0] == (0.0, 1.0, 0.0)
assert plan.TrueNorth[0] == (0.0, 1.0)
class TestEditGeoreferencingIFC2X3(test.bootstrap.IFC2X3):
def test_editing_georeferencing(self):
@@ -56,7 +47,7 @@ class TestEditGeoreferencingIFC2X3(test.bootstrap.IFC2X3):
ifcopenshell.api.georeference.edit_georeferencing(
self.file,
projected_crs={"Name": "EPSG:7856"},
map_conversion={"Eastings": 123.45, "Northings": 234.56},
coordinate_operation={"Eastings": 123.45, "Northings": 234.56},
)
conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion", verbose=True)
crs = ifcopenshell.util.element.get_pset(project, "ePSet_ProjectedCRS", verbose=True)
@@ -0,0 +1,38 @@
# 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 TestEditTrueNorth(test.bootstrap.IFC4):
def test_editing_true_north(self):
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
model = ifcopenshell.api.context.add_context(self.file, "Model")
plan = ifcopenshell.api.context.add_context(self.file, "Plan")
ifcopenshell.api.georeference.edit_true_north(self.file, true_north=[0.0, 1.0])
assert model.TrueNorth[0] == (0.0, 1.0, 0.0)
assert plan.TrueNorth[0] == (0.0, 1.0)
ifcopenshell.api.georeference.edit_true_north(self.file, true_north=[-0.5, 0.8660254])
assert np.isclose(ifcopenshell.util.geolocation.get_true_north(self.file), 30)
ifcopenshell.api.georeference.edit_true_north(self.file, true_north=30)
assert np.isclose(ifcopenshell.util.geolocation.get_true_north(self.file), 30)