From ea91f4302c0a6daaa99e2d841d7df276e2d137e7 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 8 Feb 2023 16:53:19 +1100 Subject: [PATCH] Drawings now load on-demand instead of at project load. See #2520. --- src/blenderbim/blenderbim/bim/import_ifc.py | 61 +++---------- .../bim/module/geometry/operator.py | 5 +- src/blenderbim/blenderbim/core/drawing.py | 22 +++-- src/blenderbim/blenderbim/core/tool.py | 5 + src/blenderbim/blenderbim/tool/__init__.py | 1 + src/blenderbim/blenderbim/tool/drawing.py | 91 ++++++++++++++++++- src/blenderbim/blenderbim/tool/loader.py | 54 +++++++++++ 7 files changed, 173 insertions(+), 66 deletions(-) create mode 100644 src/blenderbim/blenderbim/tool/loader.py diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index 5c9c842265..0332f4f11c 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -236,8 +236,6 @@ class IfcImporter: self.profile_code("Create materials") self.create_styles() self.profile_code("Create styles") - self.create_annotations() - self.profile_code("Create annotation") self.parse_native_elements() self.profile_code("Parsing native elements") self.create_native_elements() @@ -586,7 +584,7 @@ class IfcImporter: if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import: grid_obj.lock_location = (True, True, True) grid_obj.lock_rotation = (True, True, True) - collection = bpy.data.collections.new(self.get_name(grid)) + collection = bpy.data.collections.new(tool.Loader.get_name(grid)) u_axes = bpy.data.collections.new("UAxes") collection.children.link(u_axes) v_axes = bpy.data.collections.new("VAxes") @@ -628,11 +626,11 @@ class IfcImporter: try: shape = ifcopenshell.geom.create_shape(self.settings, representation_map.MappedRepresentation) mesh = self.create_mesh(element, shape) - self.link_mesh(shape, mesh) + tool.Loader.link_mesh(shape, mesh) self.meshes[mesh_name] = mesh except: self.ifc_import_settings.logger.error("Failed to generate shape for %s", element) - obj = bpy.data.objects.new(self.get_name(element), mesh) + obj = bpy.data.objects.new(tool.Loader.get_name(element), mesh) self.link_element(element, obj) self.material_creator.create(element, obj, mesh) self.type_products[element.GlobalId] = obj @@ -687,9 +685,6 @@ class IfcImporter: def create_spatial_elements(self): self.create_generic_elements(self.spatial_elements) - def create_annotations(self): - self.create_generic_elements(self.annotations) - def create_elements(self): self.create_generic_elements(self.elements) @@ -923,21 +918,21 @@ class IfcImporter: pass elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": mesh = self.create_camera(element, shape) - self.link_mesh(shape, mesh) + tool.Loader.link_mesh(shape, mesh) elif element.is_a("IfcAnnotation") and self.is_curve_annotation(element) and shape: mesh = self.create_curve(element, shape) - self.link_mesh(shape, mesh) + tool.Loader.link_mesh(shape, mesh) elif shape: - mesh_name = self.get_mesh_name(shape.geometry) + mesh_name = tool.Loader.get_mesh_name(shape.geometry) mesh = self.meshes.get(mesh_name) if mesh is None: mesh = self.create_mesh(element, shape) - self.link_mesh(shape, mesh) + tool.Loader.link_mesh(shape, mesh) self.meshes[mesh_name] = mesh else: mesh = None - obj = bpy.data.objects.new(self.get_name(element), mesh) + obj = bpy.data.objects.new(tool.Loader.get_name(element), mesh) self.link_element(element, obj) if shape: @@ -1385,12 +1380,6 @@ class IfcImporter: if not view_collection: view_collection = bpy.data.collections.new("Views") self.project["blender"].children.link(view_collection) - for element in self.file.by_type("IfcAnnotation"): - if element.ObjectType == "DRAWING": - group = [r for r in element.HasAssignments if r.is_a("IfcRelAssignsToGroup")][0].RelatingGroup - collection = bpy.data.collections.new("IfcGroup/" + group.Name) - self.collections[group.GlobalId] = collection - view_collection.children.link(collection) def create_spatial_decomposition_collection(self, parent, related_objects): for element in related_objects: @@ -1404,7 +1393,7 @@ class IfcImporter: collection = obj.users_collection[0] self.collections[element.GlobalId] = collection if not is_existing: - collection = bpy.data.collections.new(self.get_name(element)) + collection = bpy.data.collections.new(tool.Loader.get_name(element)) self.collections[element.GlobalId] = collection parent.children.link(collection) if element.IsDecomposedBy: @@ -1432,7 +1421,7 @@ class IfcImporter: aggregates = {} for rel_aggregate in rel_aggregates: element = rel_aggregate.RelatingObject - collection = bpy.data.collections.new(self.get_name(element)) + collection = bpy.data.collections.new(tool.Loader.get_name(element)) aggregates[element.GlobalId] = {"element": element, "collection": collection} self.collections[element.GlobalId] = collection @@ -1663,9 +1652,6 @@ class IfcImporter: else: blender_material.node_tree.links.new(coord.outputs["UV"], node.inputs["Vector"]) - def get_name(self, element): - return "{}/{}".format(element.is_a(), element.Name) - def place_objects_in_collections(self): for ifc_definition_id, obj in self.added_data.items(): if isinstance(obj, bpy.types.Object): @@ -1813,16 +1799,6 @@ class IfcImporter: ): return representation.Items[0].MappingTarget - def get_mesh_name(self, geometry): - representation_id = geometry.id - if "-" in representation_id: - representation_id = int(re.sub(r"\D", "", representation_id.split("-")[0])) - else: - representation_id = int(re.sub(r"\D", "", representation_id)) - representation = self.file.by_id(representation_id) - context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0 - return "{}/{}".format(context_id, representation_id) - def create_camera(self, element, shape): if hasattr(shape, "geometry"): geometry = shape.geometry @@ -1837,7 +1813,7 @@ class IfcImporter: height = max(y) - min(y) depth = max(z) - min(z) - camera = bpy.data.cameras.new(self.get_mesh_name(geometry)) + camera = bpy.data.cameras.new(tool.Loader.get_mesh_name(geometry)) camera.type = "ORTHO" camera.ortho_scale = width if width > height else height camera.clip_end = depth @@ -1877,7 +1853,7 @@ class IfcImporter: else: geometry = shape - curve = bpy.data.curves.new(self.get_mesh_name(geometry), type="CURVE") + curve = bpy.data.curves.new(tool.Loader.get_mesh_name(geometry), type="CURVE") curve.dimensions = "3D" curve.resolution_u = 2 @@ -1903,7 +1879,7 @@ class IfcImporter: else: geometry = shape - mesh = bpy.data.meshes.new(self.get_mesh_name(geometry)) + mesh = bpy.data.meshes.new(tool.Loader.get_mesh_name(geometry)) props = bpy.context.scene.BIMGeoreferenceProperties if ( @@ -2018,17 +1994,6 @@ class IfcImporter: self.added_data[element.id()] = obj IfcStore.link_element(element, obj) - def link_mesh(self, shape, mesh): - if hasattr(shape, "geometry"): - geometry = shape.geometry - else: - geometry = shape - if "-" in geometry.id: - mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id.split("-")[0]) - else: - # TODO: See #2002 - mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id.replace(",", "")) - def set_matrix_world(self, obj, matrix_world): obj.matrix_world = matrix_world tool.Geometry.record_object_position(obj) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index a06540d42e..1795bf1f01 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -154,16 +154,13 @@ class RemoveRepresentation(bpy.types.Operator, Operator): ) -class UpdateRepresentation(bpy.types.Operator): +class UpdateRepresentation(bpy.types.Operator, Operator): bl_idname = "bim.update_representation" bl_label = "Update Representation" bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() ifc_representation_class: bpy.props.StringProperty() - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): if context.active_object and context.active_object.mode != "OBJECT": # Ensure mode is object to prevent invalid mesh data causing CTD diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py index 657a2fbdc7..6093e2cd20 100644 --- a/src/blenderbim/blenderbim/core/drawing.py +++ b/src/blenderbim/blenderbim/core/drawing.py @@ -197,7 +197,9 @@ def update_drawing_name(ifc, drawing_tool, drawing=None, name=None): group = drawing_tool.get_drawing_group(drawing) if drawing_tool.get_name(group) != name: ifc.run("attribute.edit_attributes", product=group, attributes={"Name": name}) - drawing_tool.set_drawing_collection_name(group, drawing_tool.get_drawing_collection(drawing)) + collection = drawing_tool.get_drawing_collection(drawing) + if collection: + drawing_tool.set_drawing_collection_name(group, collection) def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None): @@ -242,14 +244,13 @@ def sync_references(ifc, collector, drawing_tool, drawing=None): should_delete_existing_annotation = False should_create_annotation = False - if annotation and ( - not reference_obj - or ifc.is_moved(reference_obj) - or ifc.is_edited(reference_obj) - or ifc.is_deleted(reference_element) - or ifc.is_deleted(annotation) - ): - should_delete_existing_annotation = True + if annotation: + if ifc.is_deleted(annotation): + should_delete_existing_annotation = True + elif reference_obj and (ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)): + should_delete_existing_annotation = True + elif not reference_obj and ifc.is_deleted(reference_element): + should_delete_existing_annotation = True if reference_obj and (should_delete_existing_annotation or not annotation): should_create_annotation = True @@ -279,5 +280,6 @@ def select_assigned_product(drawing): def activate_drawing_view(ifc, drawing_tool, drawing): camera = ifc.get_object(drawing) if not camera: - return + camera = drawing_tool.import_drawing(drawing) + drawing_tool.import_annotations_in_group(drawing_tool.get_drawing_group(drawing)) drawing_tool.activate_view(camera) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 7af448590c..2432dcc572 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -308,6 +308,11 @@ class Library: def set_editing_mode(cls, mode): pass +@interface +class Loader: + pass + + @interface class Material: def add_default_material_object(cls): pass diff --git a/src/blenderbim/blenderbim/tool/__init__.py b/src/blenderbim/blenderbim/tool/__init__.py index 97c45170b6..c7101dc8ac 100644 --- a/src/blenderbim/blenderbim/tool/__init__.py +++ b/src/blenderbim/blenderbim/tool/__init__.py @@ -31,6 +31,7 @@ from blenderbim.tool.geometry import Geometry from blenderbim.tool.georeference import Georeference from blenderbim.tool.ifc import Ifc from blenderbim.tool.library import Library +from blenderbim.tool.loader import Loader from blenderbim.tool.material import Material from blenderbim.tool.misc import Misc from blenderbim.tool.model import Model diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 2fc49256bc..8e93e837b0 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -21,6 +21,7 @@ import re import bpy import math import bmesh +import logging import mathutils import webbrowser import subprocess @@ -33,6 +34,7 @@ import blenderbim.bim.module.drawing.sheeter as sheeter import blenderbim.bim.module.drawing.scheduler as scheduler import blenderbim.bim.module.drawing.annotation as annotation import blenderbim.bim.module.drawing.helper as helper +from blenderbim.bim.module.drawing.prop import get_diagram_scales class Drawing(blenderbim.core.tool.Drawing): @@ -204,7 +206,8 @@ class Drawing(blenderbim.core.tool.Drawing): @classmethod def get_drawing_collection(cls, drawing): obj = tool.Ifc.get_object(drawing) - return obj.users_collection[0] + if obj: + return obj.users_collection[0] @classmethod def get_drawing_group(cls, drawing): @@ -296,6 +299,83 @@ class Drawing(blenderbim.core.tool.Drawing): if rel.is_a("IfcRelAssignsToProduct"): return rel.RelatingProduct + @classmethod + def import_annotations_in_group(cls, group): + elements = set( + [e for e in cls.get_group_elements(group) if e.is_a("IfcAnnotation") and e.ObjectType != "DRAWING"] + ) + logger = logging.getLogger("ImportIFC") + ifc_import_settings = blenderbim.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger) + ifc_importer = blenderbim.bim.import_ifc.IfcImporter(ifc_import_settings) + ifc_importer.file = tool.Ifc.get() + ifc_importer.calculate_unit_scale() + ifc_importer.create_generic_elements(elements) + for obj in ifc_importer.added_data.values(): + tool.Collector.assign(obj) + + @classmethod + def import_drawing(cls, drawing): + settings = ifcopenshell.geom.settings() + settings.set(settings.STRICT_TOLERANCE, True) + shape = ifcopenshell.geom.create_shape(settings, drawing) + geometry = shape.geometry + + v = geometry.verts + x = [v[i] for i in range(0, len(v), 3)] + y = [v[i + 1] for i in range(0, len(v), 3)] + z = [v[i + 2] for i in range(0, len(v), 3)] + width = max(x) - min(x) + height = max(y) - min(y) + depth = max(z) - min(z) + + camera = bpy.data.cameras.new(tool.Loader.get_mesh_name(geometry)) + camera.type = "ORTHO" + camera.ortho_scale = width if width > height else height + camera.clip_end = depth + + if width > height: + camera.BIMCameraProperties.raster_x = 1000 + camera.BIMCameraProperties.raster_y = round(1000 * (height / width)) + else: + camera.BIMCameraProperties.raster_x = round(1000 * (width / height)) + camera.BIMCameraProperties.raster_y = 1000 + + psets = ifcopenshell.util.element.get_psets(drawing) + pset = psets.get("EPset_Drawing") + if pset: + if "TargetView" in pset: + camera.BIMCameraProperties.target_view = pset["TargetView"] + if "Scale" in pset: + valid_scales = [ + i[0] for i in get_diagram_scales(None, bpy.context) if pset["Scale"] == i[0].split("|")[-1] + ] + if valid_scales: + camera.BIMCameraProperties.diagram_scale = valid_scales[0] + else: + camera.BIMCameraProperties.diagram_scale = "CUSTOM" + camera.BIMCameraProperties.custom_diagram_scale = pset["Scale"] + if "HasUnderlay" in pset: + camera.BIMCameraProperties.has_underlay = pset["HasUnderlay"] + if "HasLinework" in pset: + camera.BIMCameraProperties.has_linework = pset["HasLinework"] + if "HasAnnotation" in pset: + camera.BIMCameraProperties.has_annotation = pset["HasAnnotation"] + + tool.Loader.link_mesh(shape, camera) + + obj = bpy.data.objects.new(tool.Loader.get_name(drawing), camera) + tool.Ifc.link(drawing, obj) + + m = shape.transformation.matrix.data + mat = mathutils.Matrix( + ([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]) + ) + obj.matrix_world = mat + tool.Geometry.record_object_position(obj) + tool.Collector.assign(obj) + + return obj + @classmethod def import_drawings(cls): bpy.context.scene.DocProperties.drawings.clear() @@ -923,9 +1003,12 @@ class Drawing(blenderbim.core.tool.Drawing): @classmethod def is_drawing_active(cls): camera = bpy.context.scene.camera - return (camera and camera.type == "CAMERA" and - camera.BIMObjectProperties.ifc_definition_id and - any(a.type == 'VIEW_3D' for a in bpy.context.screen.areas)) + return ( + camera + and camera.type == "CAMERA" + and camera.BIMObjectProperties.ifc_definition_id + and any(a.type == "VIEW_3D" for a in bpy.context.screen.areas) + ) @classmethod def is_camera_orthographic(cls): diff --git a/src/blenderbim/blenderbim/tool/loader.py b/src/blenderbim/blenderbim/tool/loader.py new file mode 100644 index 0000000000..589751bbae --- /dev/null +++ b/src/blenderbim/blenderbim/tool/loader.py @@ -0,0 +1,54 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2023 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import re +import bpy +import ifcopenshell.util.element +import blenderbim.core.tool +import blenderbim.tool as tool + + +# Progressively we'll refactor loading elements into Blender objects into this +# class. This will break down the monolithic import_ifc module and allow us to +# partially load and unload objects for huge models, partial model editing, and +# supplementary objects (e.g. drawings, structural analysis models, etc). + +class Loader(blenderbim.core.tool.Loader): + @classmethod + def get_mesh_name(cls, geometry): + representation_id = geometry.id + if "-" in representation_id: + representation_id = int(re.sub(r"\D", "", representation_id.split("-")[0])) + else: + representation_id = int(re.sub(r"\D", "", representation_id)) + representation = tool.Ifc.get().by_id(representation_id) + context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0 + return "{}/{}".format(context_id, representation_id) + + @classmethod + def get_name(cls, element): + return "{}/{}".format(element.is_a(), element.Name) + + @classmethod + def link_mesh(cls, shape, mesh): + geometry = shape.geometry if hasattr(shape, "geometry") else shape + if "-" in geometry.id: + mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id.split("-")[0]) + else: + # TODO: See #2002 + mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id.replace(",", ""))