mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Aggressively refactor out functions relevant to guessing a false origin to tool.Loader
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user