From 849dfbed2885fca7b084e62507d28c96c312ab64 Mon Sep 17 00:00:00 2001 From: Cyril Waechter Date: Mon, 8 Nov 2021 23:28:44 +0100 Subject: [PATCH] [BlenderBIM] Tools to load IfcRelSpaceBoundary (#1864) * Tools to load IfcRelSpaceBoundary At scene level to load all. At space level to load ones from a specific space or a specific one. * Add a tool to colour boundaries Colour is based on its related building element. --- .../bim/module/boundary/__init__.py | 12 +- .../bim/module/boundary/operator.py | 204 ++++++++++++++++++ .../blenderbim/bim/module/boundary/ui.py | 34 ++- 3 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 src/blenderbim/blenderbim/bim/module/boundary/operator.py diff --git a/src/blenderbim/blenderbim/bim/module/boundary/__init__.py b/src/blenderbim/blenderbim/bim/module/boundary/__init__.py index 1f65558621..4a18622060 100644 --- a/src/blenderbim/blenderbim/bim/module/boundary/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/boundary/__init__.py @@ -17,9 +17,17 @@ # along with BlenderBIM Add-on. If not, see . import bpy -from . import ui +from . import ui, operator -classes = (ui.BIM_PT_boundary,) +classes = ( + operator.LoadProjectSpaceBoundaries, + operator.LoadSpaceBoundaries, + operator.LoadBoundary, + operator.SelectProjectBoundaries, + operator.ColourByRelatedBuildingElement, + ui.BIM_PT_Boundary, + ui.BIM_PT_SceneBoundaries, +) def register(): diff --git a/src/blenderbim/blenderbim/bim/module/boundary/operator.py b/src/blenderbim/blenderbim/bim/module/boundary/operator.py new file mode 100644 index 0000000000..4be2cff3e3 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/boundary/operator.py @@ -0,0 +1,204 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 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 logging +import bpy +import ifcopenshell.util.attribute +import ifcopenshell.api +import blenderbim.tool as tool +from blenderbim.bim.ifc import IfcStore +from ifcopenshell.api.boundary.data import Data +import blenderbim.bim.import_ifc as import_ifc + + +def get_boundaries_collection(blender_space): + space_collection = bpy.data.collections.get(blender_space.name) + collection_name = f"Boundaries/{blender_space.BIMObjectProperties.ifc_definition_id}" + boundaries_collection = space_collection.children.get(collection_name) + if not boundaries_collection: + boundaries_collection = bpy.data.collections.new(collection_name) + space_collection.children.link(boundaries_collection) + return boundaries_collection + + +class Loader: + def __init__(self): + self.ifc_file = None + self.logger = None + self.ifc_importer = None + self.settings = None + self.load_settings() + self.load_importer() + + def load_settings(self): + self.settings = ifcopenshell.geom.settings() + self.settings.set(self.settings.EXCLUDE_SOLIDS_AND_SURFACES, False) + self.settings.set(self.settings.USE_BREP_DATA, False) + + def load_importer(self): + self.ifc_file = tool.Ifc.get() + self.logger = logging.getLogger("ImportIFC") + ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, self.logger) + self.ifc_importer = import_ifc.IfcImporter(ifc_import_settings) + self.ifc_importer.file = self.ifc_file + + def load_boundary(self, boundary_id, blender_space): + boundary = self.ifc_file.by_id(boundary_id) + obj = tool.Ifc.get_object(boundary) + if obj: + return obj + surface = boundary.ConnectionGeometry.SurfaceOnRelatingElement + # workaround for unvalid geometry provided by Revit. See https://github.com/IfcOpenShell/IfcOpenShell/issues/635#issuecomment-770366838 + if surface.is_a("IfcCurveBoundedPlane") and not getattr(surface, "InnerBoundaries", None): + surface.InnerBoundaries = () + shape = ifcopenshell.geom.create_shape(self.settings, surface) + mesh = self.ifc_importer.create_mesh(None, shape) + obj = bpy.data.objects.new(f"{boundary.is_a()}/{boundary_id}", mesh) + obj.matrix_world = blender_space.matrix_world + boundaries_collection = get_boundaries_collection(blender_space) + boundaries_collection.objects.link(obj) + tool.Ifc.link(boundary, obj) + return obj + + +class LoadProjectSpaceBoundaries(bpy.types.Operator): + bl_idname = "bim.load_project_space_boundaries" + bl_label = "Load all project space boundaries" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + loader = Loader() + if not Data.is_loaded: + Data.load(tool.Ifc.get()) + for space_id, boundaries_id in Data.spaces.items(): + blender_space = tool.Ifc.get_object(tool.Ifc.get().by_id(space_id)) + for boundary_id in boundaries_id: + loader.load_boundary(boundary_id, blender_space) + return {"FINISHED"} + + +class LoadBoundary(bpy.types.Operator): + bl_idname = "bim.load_boundary" + bl_label = "Load boundary" + bl_options = {"REGISTER", "UNDO"} + boundary_id: bpy.props.IntProperty() + + def execute(self, context): + loader = Loader() + blender_space = context.active_object + for obj in context.visible_objects: + obj.select_set(False) + obj = loader.load_boundary(self.boundary_id, blender_space) + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + return {"FINISHED"} + + +class LoadSpaceBoundaries(bpy.types.Operator): + bl_idname = "bim.load_space_boundaries" + bl_label = "Load selected space boundaries" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + blender_space = context.active_object + oprops = context.active_object.BIMObjectProperties + loader = Loader() + if not Data.is_loaded: + Data.load(tool.Ifc.get()) + for boundary_id in Data.spaces.get(oprops.ifc_definition_id, []): + loader.load_boundary(boundary_id, blender_space) + return {"FINISHED"} + + +class SelectProjectBoundaries(bpy.types.Operator): + bl_idname = "bim.select_project_space_boundaries" + bl_label = "Select all project space boundaries" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + for obj in context.visible_objects: + obj.select_set(False) + ifc_file = tool.Ifc.get() + for boundary_id in Data.boundaries: + boundary = ifc_file.by_id(boundary_id) + obj = tool.Ifc.get_object(boundary) + if obj: + obj.select_set(True) + return {"FINISHED"} + + +def get_colour(ifc_boundary): + """Return a color depending on IfcClass given""" + product_colors = { + "IfcWall": (0.7, 0.3, 0, 1), + "IfcWindow": (0, 0.7, 1, 1), + "IfcSlab": (0.7, 0.7, 0.5, 1), + "IfcRoof": (0, 0.3, 0, 1), + "IfcDoor": (1, 1, 1, 1), + } + if ifc_boundary.PhysicalOrVirtualBoundary == "VIRTUAL": + return (1, 0, 1, 1) + + element = ifc_boundary.RelatedBuildingElement + if not element: + return (1, 0, 0, 1) + for product, colour in product_colors.items(): + if element.is_a(product): + return colour + return (0, 0, 0, 1) + + +class ColourByRelatedBuildingElement(bpy.types.Operator): + bl_idname = "bim.colour_by_related_building_element" + bl_label = "Apply colour based on related building elements" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + IfcStore.begin_transaction(self) + self.store_state(context) + result = self._execute(context) + IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) + return result + + def _execute(self, context): + self.file = tool.Ifc.get() + for obj in context.visible_objects: + if not obj.BIMObjectProperties.ifc_definition_id: + continue + element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + if not element.is_a("IfcRelSpaceBoundary"): + continue + obj.color = get_colour(element) + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if areas: + areas[0].spaces[0].shading.color_type = "OBJECT" + return {"FINISHED"} + + def store_state(self, context): + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if areas: + self.transaction_data = {"area": areas[0], "color_type": areas[0].spaces[0].shading.color_type} + + def rollback(self, data): + if data: + data["area"].spaces[0].shading.color_type = data["color_type"] + + def commit(self, data): + if data: + data["area"].spaces[0].shading.color_type = "OBJECT" diff --git a/src/blenderbim/blenderbim/bim/module/boundary/ui.py b/src/blenderbim/blenderbim/bim/module/boundary/ui.py index 405f171125..ab8cd0e6bd 100644 --- a/src/blenderbim/blenderbim/bim/module/boundary/ui.py +++ b/src/blenderbim/blenderbim/bim/module/boundary/ui.py @@ -20,12 +20,32 @@ import bpy import blenderbim.bim.helper from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore +import blenderbim.tool as tool from ifcopenshell.api.boundary.data import Data -class BIM_PT_boundary(Panel): +class BIM_PT_SceneBoundaries(Panel): bl_label = "IFC Space Boundaries" - bl_idname = "BIM_PT_boundary" + bl_id_name = "BIM_PT_scene_boundaries" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @classmethod + def poll(cls, context): + return IfcStore.get_file() + + def draw(self, context): + row = self.layout.row() + row.operator("bim.load_project_space_boundaries") + row.operator("bim.select_project_space_boundaries", text="", icon="RESTRICT_SELECT_OFF") + row.operator("bim.colour_by_related_building_element", text="", icon="BRUSH_DATA") + + +class BIM_PT_Boundary(Panel): + bl_label = "IFC Space Boundaries" + bl_idname = "BIM_PT_Boundary" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -46,9 +66,15 @@ class BIM_PT_boundary(Panel): def draw(self, context): self.oprops = context.active_object.BIMObjectProperties + row = self.layout.row() + row.operator("bim.load_space_boundaries") + ifc_file = tool.Ifc.get() if not Data.is_loaded: - Data.load(IfcStore.get_file()) + Data.load(ifc_file) for boundary_id in Data.spaces.get(self.oprops.ifc_definition_id, []): boundary = Data.boundaries[boundary_id] + building_element = ifc_file.by_id(boundary["RelatedBuildingElement"]) row = self.layout.row() - row.label(text=f"{boundary_id}", icon="GHOST_ENABLED") + row.label(text=f"{boundary_id} > {building_element.is_a()}/{building_element.Name}", icon="GHOST_ENABLED") + op = row.operator("bim.load_boundary", text="", icon="RESTRICT_SELECT_OFF") + op.boundary_id = boundary_id