Aggressively refactor out functions relevant to guessing a false origin to tool.Loader

This commit is contained in:
Dion Moult
2024-06-15 23:50:27 +10:00
parent 3a6cf0e5ae
commit 8246d2c593
3 changed files with 288 additions and 245 deletions
+39 -242
View File
@@ -217,9 +217,6 @@ class IfcImporter:
self.ifc_import_settings = ifc_import_settings self.ifc_import_settings = ifc_import_settings
self.diff = None self.diff = None
self.file: ifcopenshell.file = None self.file: ifcopenshell.file = None
self.context_settings: list[ifcopenshell.geom.main.settings] = []
self.gross_context_settings: list[ifcopenshell.geom.main.settings] = []
self.contexts = []
self.project = None self.project = None
self.has_existing_project = False self.has_existing_project = False
# element guids to blender collections mapping # element guids to blender collections mapping
@@ -255,6 +252,7 @@ class IfcImporter:
bpy.context.window_manager.progress_update(self.progress) bpy.context.window_manager.progress_update(self.progress)
def execute(self) -> None: def execute(self) -> None:
tool.Loader.set_settings(self.ifc_import_settings)
bpy.context.window_manager.progress_begin(0, 100) bpy.context.window_manager.progress_begin(0, 100)
self.profile_code("Starting import process") self.profile_code("Starting import process")
self.load_file() self.load_file()
@@ -313,111 +311,10 @@ class IfcImporter:
self.update_progress(100) self.update_progress(100)
bpy.context.window_manager.progress_end() bpy.context.window_manager.progress_end()
def is_element_far_away(self, element: ifcopenshell.entity_instance) -> bool:
try:
placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
point = placement[:, 3][0:3]
return self.is_point_far_away(point, is_meters=False)
except:
return False
def is_point_far_away(
self, point: Union[ifcopenshell.entity_instance, npt.NDArray[np.float64]], is_meters: bool = True
) -> bool:
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
# Users can configure this if they have to handle larger sites but beware of surveying precision
limit = self.ifc_import_settings.distance_limit
limit = limit if is_meters else (limit / self.unit_scale)
coords = getattr(point, "Coordinates", point)
return abs(coords[0]) > limit or abs(coords[1]) > limit or abs(coords[2]) > limit
def process_context_filter(self) -> None: def process_context_filter(self) -> None:
# Annotation ContextType is to accommodate broken Revit files tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(self.file)
# See https://github.com/Autodesk/revit-ifc/issues/187 tool.Loader.settings.context_settings = tool.Loader.create_settings()
type_priority = ["Model", "Plan", "Annotation"] tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
identifier_priority = [
"Body",
"Body-FallBack",
"Facetation",
"FootPrint",
"Profile",
"Surface",
"Reference",
"Axis",
"Clearance",
"Box",
"Lighting",
"Annotation",
"CoG",
]
target_view_priority = [
"MODEL_VIEW",
"PLAN_VIEW",
"REFLECTED_PLAN_VIEW",
"ELEVATION_VIEW",
"SECTION_VIEW",
"GRAPH_VIEW",
"SKETCH_VIEW",
"USERDEFINED",
"NOTDEFINED",
]
def sort_context(context):
priority = []
if context.ContextType in type_priority:
priority.append(len(type_priority) - type_priority.index(context.ContextType))
else:
priority.append(0)
return tuple(priority)
def sort_subcontext(context):
priority = []
if context.ContextType in type_priority:
priority.append(len(type_priority) - type_priority.index(context.ContextType))
else:
priority.append(0)
if context.ContextIdentifier in identifier_priority:
priority.append(len(identifier_priority) - identifier_priority.index(context.ContextIdentifier))
else:
priority.append(0)
if context.TargetView in target_view_priority:
priority.append(len(target_view_priority) - target_view_priority.index(context.TargetView))
else:
priority.append(0)
priority.append(context.TargetScale or 0) # Big then small
return tuple(priority)
# Ideally, all representations should be in a subcontext, but some BIM programs don't do this correctly
self.contexts = sorted(
self.file.by_type("IfcGeometricRepresentationSubContext"), key=sort_subcontext, reverse=True
) + sorted(
self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False),
key=sort_context,
reverse=True,
)
for context in self.contexts:
settings = ifcopenshell.geom.settings()
settings.set("mesher-linear-deflection", self.ifc_import_settings.deflection_tolerance)
settings.set("mesher-angular-deflection", self.ifc_import_settings.angular_tolerance)
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
settings.set("context-ids", [context.id()])
settings.set("apply-default-materials", False)
self.context_settings.append(settings)
settings = ifcopenshell.geom.settings()
settings.set("mesher-linear-deflection", self.ifc_import_settings.deflection_tolerance)
settings.set("mesher-angular-deflection", self.ifc_import_settings.angular_tolerance)
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
settings.set("disable-opening-subtractions", True)
settings.set("context-ids", [context.id()])
settings.set("apply-default-materials", False)
self.gross_context_settings.append(settings)
def process_element_filter(self) -> None: def process_element_filter(self) -> None:
offset = self.ifc_import_settings.element_offset offset = self.ifc_import_settings.element_offset
@@ -504,8 +401,8 @@ class IfcImporter:
context = None context = None
for rep in element.Representation.Representations: for rep in element.Representation.Representations:
if rep.ContextOfItems in self.contexts: if rep.ContextOfItems in tool.Loader.settings.contexts:
rep_priority = self.contexts.index(rep.ContextOfItems) rep_priority = tool.Loader.settings.contexts.index(rep.ContextOfItems)
if representation is None or rep_priority < representation_priority: if representation is None or rep_priority < representation_priority:
representation = rep representation = rep
representation_priority = rep_priority representation_priority = rep_priority
@@ -526,7 +423,7 @@ class IfcImporter:
if representation_id is None: if representation_id is None:
representation_id = rep.id() representation_id = rep.id()
rep = rep.Items[0].MappingSource.MappedRepresentation rep = rep.Items[0].MappingSource.MappedRepresentation
if not rep: # Accommodate invalid files if not rep: # Accommodate invalid files
return False return False
else: else:
if representation_id is None: if representation_id is None:
@@ -646,105 +543,18 @@ class IfcImporter:
if props.has_blender_offset: if props.has_blender_offset:
return return
if self.ifc_import_settings.false_origin: if self.ifc_import_settings.false_origin:
return self.set_manual_blender_offset() return tool.Loader.set_manual_blender_offset()
if self.file.schema == "IFC2X3": if self.file.schema == "IFC2X3":
project = self.file.by_type("IfcProject")[0] project = self.file.by_type("IfcProject")[0]
else: else:
project = self.file.by_type("IfcContext")[0] project = self.file.by_type("IfcContext")[0]
site = self.find_decomposed_ifc_class(project, "IfcSite") site = tool.Loader.find_decomposed_ifc_class(project, "IfcSite")
if site and self.is_element_far_away(site): if site and tool.Loader.is_element_far_away(site):
return self.guess_false_origin_and_project_north(site) return tool.Loader.guess_false_origin_and_project_north(site)
building = self.find_decomposed_ifc_class(project, "IfcBuilding") building = tool.Loader.find_decomposed_ifc_class(project, "IfcBuilding")
if building and self.is_element_far_away(building): if building and tool.Loader.is_element_far_away(building):
return self.guess_false_origin_and_project_north(building) return tool.Loader.guess_false_origin_and_project_north(building)
return self.guess_false_origin() return tool.Loader.guess_false_origin_from_elements(self.file)
def set_manual_blender_offset(self) -> None:
props = bpy.context.scene.BIMGeoreferenceProperties
props.blender_eastings = str(self.ifc_import_settings.false_origin[0])
props.blender_northings = str(self.ifc_import_settings.false_origin[1])
props.blender_orthogonal_height = str(self.ifc_import_settings.false_origin[2])
props.has_blender_offset = True
def guess_false_origin_and_project_north(self, element: ifcopenshell.entity_instance) -> None:
if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"):
return
placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
props = bpy.context.scene.BIMGeoreferenceProperties
props.blender_eastings = str(placement[0][3])
props.blender_northings = str(placement[1][3])
props.blender_orthogonal_height = str(placement[2][3])
x_axis = mathutils.Vector(placement[:, 0][0:3])
default_x_axis = mathutils.Vector((1, 0, 0))
if (default_x_axis - x_axis).length > 0.01:
props.blender_x_axis_abscissa = str(placement[0][0])
props.blender_x_axis_ordinate = str(placement[1][0])
props.has_blender_offset = True
def guess_false_origin(self) -> None:
# Civil BIM applications like to work in absolute coordinates, where the
# ObjectPlacement is usually 0,0,0 (but not always, so we'll need to
# check for the actual transformation) but each individual coordinate of
# the shape representation is in absolute values.
offset_point = self.get_offset_point()
if offset_point is None:
return
props = bpy.context.scene.BIMGeoreferenceProperties
props.blender_eastings = str(offset_point[0])
props.blender_northings = str(offset_point[1])
props.blender_orthogonal_height = str(offset_point[2])
props.has_blender_offset = True
def get_offset_point(self) -> Union[npt.NDArray[np.float64], None]:
elements_checked = 0
# If more than these elements aren't far away, the file probably isn't absolutely positioned
element_checking_threshold = 3
for element in self.file.by_type("IfcElement"):
if not element.Representation:
continue
elements_checked += 1
if elements_checked > element_checking_threshold:
return
if element.ObjectPlacement and element.ObjectPlacement.is_a("IfcLocalPlacement"):
placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)[:, 3][0:3]
if self.is_point_far_away(placement, is_meters=False):
return placement
if not self.does_element_likely_have_geometry_far_away(element):
continue
shape = self.create_generic_shape(element)
if not shape:
continue
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
point = mat @ np.array(
(
shape.geometry.verts[0],
shape.geometry.verts[1],
shape.geometry.verts[2],
0.0,
)
)
point = point / self.unit_scale
if self.is_point_far_away(point, is_meters=False):
return point
def does_element_likely_have_geometry_far_away(self, element: ifcopenshell.entity_instance) -> bool:
for representation in element.Representation.Representations:
items = []
for item in representation.Items:
if item.is_a("IfcMappedItem"):
items.extend(item.MappingSource.MappedRepresentation.Items)
else:
items.append(item)
for item in items:
for subelement in self.file.traverse(item):
if subelement.is_a("IfcCartesianPointList3D"):
for point in subelement.CoordList:
if len(point) == 3 and self.is_point_far_away(point, is_meters=False):
return True
if subelement.is_a("IfcCartesianPoint"):
if len(subelement.Coordinates) == 3 and self.is_point_far_away(subelement, is_meters=False):
return True
return False
def apply_blender_offset_to_matrix_world(self, obj: bpy.types.Object, matrix: np.ndarray) -> mathutils.Matrix: def apply_blender_offset_to_matrix_world(self, obj: bpy.types.Object, matrix: np.ndarray) -> mathutils.Matrix:
props = bpy.context.scene.BIMGeoreferenceProperties props = bpy.context.scene.BIMGeoreferenceProperties
@@ -782,18 +592,6 @@ class IfcImporter:
return mathutils.Matrix(matrix.tolist()) return mathutils.Matrix(matrix.tolist())
def find_decomposed_ifc_class(
self, element: ifcopenshell.entity_instance, ifc_class: str
) -> Union[ifcopenshell.entity_instance, None]:
if element.is_a(ifc_class):
return element
rel_aggregates = element.IsDecomposedBy
for rel_aggregate in rel_aggregates:
for part in rel_aggregate.RelatedObjects:
result = self.find_decomposed_ifc_class(part, ifc_class)
if result:
return result
def create_grids(self): def create_grids(self):
if not self.ifc_import_settings.should_load_geometry: if not self.ifc_import_settings.should_load_geometry:
return return
@@ -805,7 +603,7 @@ class IfcImporter:
self.ifc_import_settings.logger.error("An invalid grid was found %s", grid) self.ifc_import_settings.logger.error("An invalid grid was found %s", grid)
continue continue
if grid.Representation: if grid.Representation:
shape = self.create_generic_shape(grid) shape = tool.Loader.create_generic_shape(grid)
grid_obj = self.create_product(grid, shape) grid_obj = self.create_product(grid, shape)
grid_placement = self.get_element_matrix(grid) grid_placement = self.get_element_matrix(grid)
if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import: if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import:
@@ -818,7 +616,7 @@ class IfcImporter:
def create_grid_axes(self, axes, grid_obj, grid_placement): def create_grid_axes(self, axes, grid_obj, grid_placement):
for axis in axes: for axis in axes:
shape = self.create_generic_shape(axis.AxisCurve) shape = tool.Loader.create_generic_shape(axis.AxisCurve)
mesh = self.create_mesh(axis, shape) mesh = self.create_mesh(axis, shape)
obj = bpy.data.objects.new(f"IfcGridAxis/{axis.AxisTag}", mesh) obj = bpy.data.objects.new(f"IfcGridAxis/{axis.AxisTag}", mesh)
if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import: if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import:
@@ -837,14 +635,14 @@ class IfcImporter:
self.ifc_import_settings.logger.info("Creating object %s", element) self.ifc_import_settings.logger.info("Creating object %s", element)
mesh = None mesh = None
if self.ifc_import_settings.should_load_geometry: if self.ifc_import_settings.should_load_geometry:
for context in self.contexts: for context in tool.Loader.settings.contexts:
representation = ifcopenshell.util.representation.get_representation(element, context) representation = ifcopenshell.util.representation.get_representation(element, context)
if not representation: if not representation:
continue continue
mesh_name = "{}/{}".format(representation.ContextOfItems.id(), representation.id()) mesh_name = "{}/{}".format(representation.ContextOfItems.id(), representation.id())
mesh = self.meshes.get(mesh_name) mesh = self.meshes.get(mesh_name)
if mesh is None: if mesh is None:
shape = self.create_generic_shape(representation) shape = tool.Loader.create_generic_shape(representation)
if shape: if shape:
mesh = self.create_mesh(element, shape) mesh = self.create_mesh(element, shape)
tool.Loader.link_mesh(shape, mesh) tool.Loader.link_mesh(shape, mesh)
@@ -897,28 +695,19 @@ class IfcImporter:
def create_elements(self) -> None: def create_elements(self) -> None:
self.create_generic_elements(self.elements) self.create_generic_elements(self.elements)
tmp = self.context_settings self.create_generic_elements(self.gross_elements, is_gross=True)
self.context_settings = self.gross_context_settings
self.create_generic_elements(self.gross_elements)
self.context_settings = tmp
def create_generic_shape( def create_generic_elements(
self, element: ifcopenshell.entity_instance self, elements: set[ifcopenshell.entity_instance], unselectable=False, is_gross=False
) -> Union[ifcopenshell.geom.ShapeElementType, None]: ) -> None:
for settings in self.context_settings:
try:
result = ifcopenshell.geom.create_shape(settings, element)
if result:
return result
except:
pass
def create_generic_elements(self, elements: set[ifcopenshell.entity_instance], unselectable=False) -> None:
if isinstance(self.file, ifcopenshell.sqlite): if isinstance(self.file, ifcopenshell.sqlite):
return self.create_generic_sqlite_elements(elements) return self.create_generic_sqlite_elements(elements)
if self.ifc_import_settings.should_load_geometry: if self.ifc_import_settings.should_load_geometry:
for settings in self.context_settings: context_settings = (
tool.Loader.settings.gross_context_settings if is_gross else tool.Loader.settings.context_settings
)
for settings in context_settings:
if not elements: if not elements:
break break
products = self.create_products(elements, settings=settings) products = self.create_products(elements, settings=settings)
@@ -992,7 +781,7 @@ class IfcImporter:
results = set() results = set()
if not products: if not products:
return results return results
if self.ifc_import_settings.should_use_cpu_multiprocessing: if tool.Loader.settings.should_use_cpu_multiprocessing:
iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count(), include=products) iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count(), include=products)
else: else:
iterator = ifcopenshell.geom.iterator(settings, self.file, include=products) iterator = ifcopenshell.geom.iterator(settings, self.file, include=products)
@@ -1216,7 +1005,7 @@ class IfcImporter:
mesh = bpy.data.meshes.new("Native") mesh = bpy.data.meshes.new("Native")
props = bpy.context.scene.BIMGeoreferenceProperties props = bpy.context.scene.BIMGeoreferenceProperties
if props.has_blender_offset and self.is_point_far_away(self.mesh_data["co"][0:3], is_meters=False): if props.has_blender_offset and tool.Loader.is_point_far_away(self.mesh_data["co"][0:3], is_meters=False):
verts_array = np.array(self.mesh_data["co"]) verts_array = np.array(self.mesh_data["co"])
verts_array *= self.unit_scale verts_array *= self.unit_scale
offset_x, offset_y, offset_z = verts_array[0:3] offset_x, offset_y, offset_z = verts_array[0:3]
@@ -1368,7 +1157,7 @@ class IfcImporter:
matrix[1][3] *= self.unit_scale matrix[1][3] *= self.unit_scale
matrix[2][3] *= self.unit_scale matrix[2][3] *= self.unit_scale
# TODO: support inner radius, start param, and end param # TODO: support inner radius, start param, and end param
geometry = self.create_generic_shape(item.Directrix) geometry = tool.Loader.create_generic_shape(item.Directrix)
e = geometry.edges e = geometry.edges
v = geometry.verts v = geometry.verts
vertices = [list(matrix @ [v[i], v[i + 1], v[i + 2], 1]) for i in range(0, len(v), 3)] vertices = [list(matrix @ [v[i], v[i + 1], v[i + 2], 1]) for i in range(0, len(v), 3)]
@@ -1458,6 +1247,7 @@ class IfcImporter:
def calculate_unit_scale(self): def calculate_unit_scale(self):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
tool.Loader.set_unit_scale(self.unit_scale)
def set_units(self): def set_units(self):
units = self.file.by_type("IfcUnitAssignment")[0] units = self.file.by_type("IfcUnitAssignment")[0]
@@ -1631,7 +1421,9 @@ class IfcImporter:
if ( if (
props.has_blender_offset props.has_blender_offset
and geometry.verts and geometry.verts
and self.is_point_far_away((geometry.verts[0], geometry.verts[1], geometry.verts[2])) and tool.Loader.is_point_far_away(
(geometry.verts[0], geometry.verts[1], geometry.verts[2]), is_meters=True
)
): ):
# Shift geometry close to the origin based off that first vert it found # Shift geometry close to the origin based off that first vert it found
verts_array = np.array(geometry.verts) verts_array = np.array(geometry.verts)
@@ -1765,6 +1557,8 @@ class IfcImportSettings:
self.deflection_tolerance = 0.001 self.deflection_tolerance = 0.001
self.angular_tolerance = 0.5 self.angular_tolerance = 0.5
self.void_limit = 30 self.void_limit = 30
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
# Users can configure this if they have to handle larger sites but beware of surveying precision
self.distance_limit = 1000 self.distance_limit = 1000
self.false_origin = None self.false_origin = None
self.element_offset = 0 self.element_offset = 0
@@ -1772,6 +1566,9 @@ class IfcImportSettings:
self.has_filter = None self.has_filter = None
self.should_filter_spatial_elements = True self.should_filter_spatial_elements = True
self.should_setup_viewport_camera = True self.should_setup_viewport_camera = True
self.contexts = []
self.context_settings: list[ifcopenshell.geom.main.settings] = []
self.gross_context_settings: list[ifcopenshell.geom.main.settings] = []
self.elements: set[ifcopenshell.entity_instance] = set() self.elements: set[ifcopenshell.entity_instance] = set()
@staticmethod @staticmethod
+158 -1
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License # 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/>. # along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import re import re
import bpy import bpy
import bmesh import bmesh
@@ -23,8 +24,8 @@ import ifcopenshell.geom
import ifcopenshell.util.element import ifcopenshell.util.element
import blenderbim.core.tool import blenderbim.core.tool
import blenderbim.tool as tool import blenderbim.tool as tool
import os
import numpy as np import numpy as np
import numpy.typing as npt
from mathutils import Vector from mathutils import Vector
from pathlib import Path from pathlib import Path
from typing import Union from typing import Union
@@ -40,6 +41,17 @@ OBJECT_DATA_TYPE = Union[bpy.types.Mesh, bpy.types.Curve]
class Loader(blenderbim.core.tool.Loader): class Loader(blenderbim.core.tool.Loader):
unit_scale: float = 1
settings = None
@classmethod
def set_unit_scale(cls, unit_scale: float) -> None:
cls.unit_scale = unit_scale
@classmethod
def set_settings(cls, settings) -> None:
cls.settings = settings
@classmethod @classmethod
def create_project_collection(cls, name: str) -> bpy.types.Collection: def create_project_collection(cls, name: str) -> bpy.types.Collection:
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
@@ -508,3 +520,148 @@ class Loader(blenderbim.core.tool.Loader):
# Finish up, write the bmesh back to the mesh # Finish up, write the bmesh back to the mesh
bm.to_mesh(mesh) bm.to_mesh(mesh)
bm.free() bm.free()
@classmethod
def is_point_far_away(
cls, point: Union[ifcopenshell.entity_instance, npt.NDArray[np.float64]], is_meters: bool = True
) -> bool:
limit = cls.settings.distance_limit
limit = limit if is_meters else (limit / cls.unit_scale)
coords = getattr(point, "Coordinates", point)
return abs(coords[0]) > limit or abs(coords[1]) > limit or abs(coords[2]) > limit
@classmethod
def is_element_far_away(cls, element: ifcopenshell.entity_instance) -> bool:
try:
placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
point = placement[:, 3][0:3]
return tool.Loader.is_point_far_away(point, is_meters=False)
except:
return False
@classmethod
def create_settings(cls, is_gross=False):
results = []
for context in cls.settings.contexts:
settings = ifcopenshell.geom.settings()
settings.set("mesher-linear-deflection", cls.settings.deflection_tolerance)
settings.set("mesher-angular-deflection", cls.settings.angular_tolerance)
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
settings.set("context-ids", [context.id()])
settings.set("apply-default-materials", False)
if is_gross:
settings.set("disable-opening-subtractions", True)
results.append(settings)
return results
@classmethod
def set_manual_blender_offset(cls) -> None:
props = bpy.context.scene.BIMGeoreferenceProperties
props.blender_eastings = str(cls.settings.false_origin[0])
props.blender_northings = str(cls.settings.false_origin[1])
props.blender_orthogonal_height = str(cls.settings.false_origin[2])
props.has_blender_offset = True
@classmethod
def guess_false_origin_and_project_north(cls, element: ifcopenshell.entity_instance) -> None:
if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"):
return
placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
props = bpy.context.scene.BIMGeoreferenceProperties
props.blender_eastings = str(placement[0][3])
props.blender_northings = str(placement[1][3])
props.blender_orthogonal_height = str(placement[2][3])
x_axis = mathutils.Vector(placement[:, 0][0:3])
default_x_axis = mathutils.Vector((1, 0, 0))
if (default_x_axis - x_axis).length > 0.01:
props.blender_x_axis_abscissa = str(placement[0][0])
props.blender_x_axis_ordinate = str(placement[1][0])
props.has_blender_offset = True
@classmethod
def find_decomposed_ifc_class(
cls, element: ifcopenshell.entity_instance, ifc_class: str
) -> Union[ifcopenshell.entity_instance, None]:
if element.is_a(ifc_class):
return element
rel_aggregates = element.IsDecomposedBy
for rel_aggregate in rel_aggregates:
for part in rel_aggregate.RelatedObjects:
result = cls.find_decomposed_ifc_class(part, ifc_class)
if result:
return result
@classmethod
def create_generic_shape(
cls, element: ifcopenshell.entity_instance
) -> Union[ifcopenshell.geom.ShapeElementType, None]:
for settings in cls.settings.context_settings:
try:
result = ifcopenshell.geom.create_shape(settings, element)
if result:
return result
except:
pass
@classmethod
def does_element_likely_have_geometry_far_away(
cls, ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance
) -> bool:
for representation in element.Representation.Representations:
items = []
for item in representation.Items:
if item.is_a("IfcMappedItem"):
items.extend(item.MappingSource.MappedRepresentation.Items)
else:
items.append(item)
for item in items:
for subelement in ifc_file.traverse(item):
if subelement.is_a("IfcCartesianPointList3D"):
for point in subelement.CoordList:
if len(point) == 3 and cls.is_point_far_away(point, is_meters=False):
return True
if subelement.is_a("IfcCartesianPoint"):
if len(subelement.Coordinates) == 3 and cls.is_point_far_away(subelement, is_meters=False):
return True
return False
@classmethod
def get_offset_point(cls, ifc_file: ifcopenshell.file) -> Union[npt.NDArray[np.float64], None]:
elements_checked = 0
# If more than these elements aren't far away, the file probably isn't absolutely positioned
element_checking_threshold = 3
for element in ifc_file.by_type("IfcElement"):
if not element.Representation:
continue
elements_checked += 1
if elements_checked > element_checking_threshold:
return
if element.ObjectPlacement and element.ObjectPlacement.is_a("IfcLocalPlacement"):
placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)[:, 3][0:3]
if cls.is_point_far_away(placement, is_meters=False):
return placement
if not cls.does_element_likely_have_geometry_far_away(ifc_file, element):
continue
shape = cls.create_generic_shape(element)
if not shape:
continue
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
point = mat @ np.array((shape.geometry.verts[0], shape.geometry.verts[1], shape.geometry.verts[2], 0.0))
point = point / cls.unit_scale
if cls.is_point_far_away(point, is_meters=False):
return point
@classmethod
def guess_false_origin_from_elements(cls, ifc_file: ifcopenshell.file) -> None:
# Civil BIM applications like to work in absolute coordinates, where the
# ObjectPlacement is usually 0,0,0 (but not always, so we'll need to
# check for the actual transformation) but each individual coordinate of
# the shape representation is in absolute values.
offset_point = cls.get_offset_point(ifc_file)
if offset_point is None:
return
props = bpy.context.scene.BIMGeoreferenceProperties
props.blender_eastings = str(offset_point[0])
props.blender_northings = str(offset_point[1])
props.blender_orthogonal_height = str(offset_point[2])
props.has_blender_offset = True
@@ -75,6 +75,14 @@ def get_representation(
subcontext: Optional[str] = None, subcontext: Optional[str] = None,
target_view: Optional[str] = None, target_view: Optional[str] = None,
) -> Union[ifcopenshell.entity_instance, None]: ) -> Union[ifcopenshell.entity_instance, None]:
"""Gets a IfcShapeRepresentation filtered by the context type, identifier, and target view
:param element: An IfcProduct or IfcTypeProduct
:param context: Either a specific IfcGeometricRepresentationContext or a ContextType
:param subcontext: A ContextIdentifier string, or any if left blank.
:param target_view: A TargetView string, or any if left blank.
:return: The first IfcShapeRepresentation matching the criteria.
"""
if element.is_a("IfcProduct") and element.Representation: if element.is_a("IfcProduct") and element.Representation:
for r in element.Representation.Representations: for r in element.Representation.Representations:
if is_representation_of_context(r, context, subcontext, target_view): if is_representation_of_context(r, context, subcontext, target_view):
@@ -89,9 +97,7 @@ def resolve_representation(representation: ifcopenshell.entity_instance) -> ifco
"""Resolve possibly mapped representation. """Resolve possibly mapped representation.
:param representation: IfcRepresentation :param representation: IfcRepresentation
:type representation: ifcopenshell.entity_instance
:return: Representation resolved from mappings :return: Representation resolved from mappings
:rtype: ifcopenshell.entity_instance
""" """
if len(representation.Items) == 1 and representation.Items[0].is_a("IfcMappedItem"): if len(representation.Items) == 1 and representation.Items[0].is_a("IfcMappedItem"):
return resolve_representation(representation.Items[0].MappingSource.MappedRepresentation) return resolve_representation(representation.Items[0].MappingSource.MappedRepresentation)
@@ -118,3 +124,86 @@ def resolve_items(
else: else:
results.append(ResolvedItemDict(matrix=matrix.copy(), item=item)) results.append(ResolvedItemDict(matrix=matrix.copy(), item=item))
return results return results
def get_prioritised_contexts(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]:
"""Gets a list of contexts ordered from high priority to low priority
Models can contain multiple geometric contexts. When visualising models,
you may want to prioritise visualising certain contexts over others,
determined by the context type, identifier, target view, and target scale.
The default prioritises subcontexts, then contexts. It then prioritises 3D,
then 2D. It then prioritises bodies, then others. It also prioritises model
views, then plan views, then others.
:param ifc_file: The model containing contexts
:return: A list of IfcGeometricRepresentationContext (or SubContext) from
high priority to low priority.
"""
# Annotation ContextType is to accommodate broken Revit files
# See https://github.com/Autodesk/revit-ifc/issues/187
type_priority = ["Model", "Plan", "Annotation"]
identifier_priority = [
"Body",
"Body-FallBack",
"Facetation",
"FootPrint",
"Profile",
"Surface",
"Reference",
"Axis",
"Clearance",
"Box",
"Lighting",
"Annotation",
"CoG",
]
target_view_priority = [
"MODEL_VIEW",
"PLAN_VIEW",
"REFLECTED_PLAN_VIEW",
"ELEVATION_VIEW",
"SECTION_VIEW",
"GRAPH_VIEW",
"SKETCH_VIEW",
"USERDEFINED",
"NOTDEFINED",
]
def sort_context(context):
priority = []
if context.ContextType in type_priority:
priority.append(len(type_priority) - type_priority.index(context.ContextType))
else:
priority.append(0)
return tuple(priority)
def sort_subcontext(context):
priority = []
if context.ContextType in type_priority:
priority.append(len(type_priority) - type_priority.index(context.ContextType))
else:
priority.append(0)
if context.ContextIdentifier in identifier_priority:
priority.append(len(identifier_priority) - identifier_priority.index(context.ContextIdentifier))
else:
priority.append(0)
if context.TargetView in target_view_priority:
priority.append(len(target_view_priority) - target_view_priority.index(context.TargetView))
else:
priority.append(0)
priority.append(context.TargetScale or 0) # Big then small
return tuple(priority)
# Ideally, all representations should be in a subcontext, but some BIM programs don't do this correctly
return sorted(ifc_file.by_type("IfcGeometricRepresentationSubContext"), key=sort_subcontext, reverse=True) + sorted(
ifc_file.by_type("IfcGeometricRepresentationContext", include_subtypes=False),
key=sort_context,
reverse=True,
)