From 20d44378d946b9a6bf7d37dcebe85b0cf60d027d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 27 Feb 2023 20:49:38 +1100 Subject: [PATCH] You can now generate type-based spaces and regenerate spaces when boundaries change. --- .../blenderbim/bim/module/model/__init__.py | 6 +- .../blenderbim/bim/module/model/product.py | 165 ------------------ .../blenderbim/bim/module/model/prop.py | 8 - .../blenderbim/bim/module/model/space.py | 156 +++++++++++++++++ .../blenderbim/bim/module/model/ui.py | 82 +-------- .../blenderbim/bim/module/model/workspace.py | 19 +- src/blenderbim/blenderbim/tool/ifc.py | 4 + 7 files changed, 177 insertions(+), 263 deletions(-) create mode 100644 src/blenderbim/blenderbim/bim/module/model/space.py diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 335ca65145..b2239eb128 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -26,6 +26,7 @@ from . import ( product, wall, slab, + space, stair, window, opening, @@ -47,11 +48,8 @@ classes = ( product.AddEmptyType, product.AlignProduct, product.ChangeTypePage, - product.DisplayConstrTypes, product.GenerateHippedRoof, - product.GenerateSpace, product.LoadTypeThumbnails, - product.ReinvokeOperator, workspace.Hotkey, wall.AlignWall, wall.ChangeExtrusionDepth, @@ -87,6 +85,7 @@ classes = ( slab.EnableEditingSketchExtrusionProfile, slab.ResetVertex, slab.SetArcIndex, + space.GenerateSpace, prop.ConstrTypeInfo, prop.ConstrClassInfo, prop.ConstrBrowserState, @@ -102,7 +101,6 @@ classes = ( ui.BIM_PT_sverchok, ui.BIM_PT_window, ui.BIM_PT_door, - ui.DisplayConstrTypesUI, ui.LaunchTypeManager, ui.HelpConstrTypes, ui.BIM_MT_model, diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index 680ff386b4..dea07e116f 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -31,7 +31,6 @@ import blenderbim.core.geometry from . import wall, slab, profile, mep from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.model.data import AuthoringData -from blenderbim.bim.module.model.prop import store_cursor_position from mathutils import Vector, Matrix from bpy_extras.object_utils import AddObjectHelper from . import prop @@ -223,66 +222,6 @@ class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} -class DisplayConstrTypes(bpy.types.Operator): - bl_idname = "bim.display_constr_types" - bl_label = "Browse Construction Types" - bl_options = {"REGISTER"} - bl_description = "Display all available Construction Types to add new instances" - - def invoke(self, context, event): - if not AuthoringData.is_loaded: - AuthoringData.load() - props = context.scene.BIMModelProperties - ifc_class = props.ifc_class - constr_class_info = AuthoringData.constr_class_info(ifc_class) - if constr_class_info is None or not constr_class_info.fully_loaded: - AuthoringData.assetize_constr_class(ifc_class) - bpy.ops.bim.display_constr_types_ui("INVOKE_DEFAULT") - return {"FINISHED"} - - -class ReinvokeOperator(bpy.types.Operator): - bl_idname = "bim.reinvoke_operator" - bl_label = "Reinvoke Popup Operator" - bl_options = {"REGISTER"} - bl_description = "Reinvoke a popup operator" - operator: bpy.props.StringProperty() - - def execute(self, context): - return {"FINISHED"} - - def invoke(self, context, event): - browser_state = context.scene.BIMModelProperties.constr_browser_state - store_cursor_position(context, event, window=False) - cursor_x, cursor_y = browser_state.cursor_x, browser_state.cursor_y - window_x, window_y = browser_state.window_x, browser_state.window_y - window = context.window - operator = self.operator - self.move_cursor_away(context, window) - - def move_cursor_to_window(): - window.cursor_warp(window_x, window_y) - - def run_operator(operator, *args, **kwargs): - reduce(lambda x, arg: getattr(x, arg), operator.split("."), bpy.ops)(*args, **kwargs) - - def reinvoke(): - run_operator(operator, "INVOKE_DEFAULT", reinvoked=True) - window.cursor_warp(cursor_x, cursor_y) - - bpy.app.timers.register(move_cursor_to_window, first_interval=browser_state.update_delay) - bpy.app.timers.register(reinvoke, first_interval=3 * browser_state.update_delay) - return {"FINISHED"} - - def move_cursor_away(self, context, window): # closes current popup - browser_state = context.scene.BIMModelProperties.constr_browser_state - window.cursor_warp(browser_state.far_away_x, browser_state.far_away_y) - - @staticmethod - def run_operator(operator, *args, **kwargs): - reduce(lambda x, arg: getattr(x, arg), operator.split("."), bpy.ops)(*args, **kwargs) - - class AlignProduct(bpy.types.Operator): bl_idname = "bim.align_product" bl_label = "Align Product" @@ -579,110 +518,6 @@ def ensure_material_unassigned(usecase_path, ifc_file, settings): total_removed += 1 -class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.generate_space" - bl_label = "Generate Space" - bl_options = {"REGISTER"} - - def _execute(self, context): - # Note: this experimental operator relies on Shapely v2 (not v1.8 which - # is currently bundled). It only works based on a 2D plan only - # considering the standard layered walls (i.e. prismatic) in the - # currently active storey (though we can easily extend it to include - # other "bounding" elements). For now we generate rooms that exclude - # walls (i.e. not to wall midpoint or exterior / interior edge). - - import bmesh - import shapely - from math import pi - - collection = context.view_layer.active_layer_collection.collection - collection_obj = bpy.data.objects.get(collection.name) - if not collection_obj: - return - spatial_element = tool.Ifc.get_entity(collection_obj) - if not spatial_element: - return - - boundary_elements = [] - for subelement in ifcopenshell.util.element.get_decomposition(spatial_element): - if subelement.is_a("IfcWall") and tool.Model.get_usage_type(subelement) == "LAYER2": - boundary_elements.append(subelement) - - boundary_lines = [] - for element in boundary_elements: - obj = tool.Ifc.get_object(element) - if not obj: - continue - axis = self.get_wall_axis(obj) - for ypos in ["miny", "maxy"]: - start, end = axis[ypos] - offset = (end - start).normalized() * 0.3 # Offset wall lines by 300mm - boundary_lines.append(shapely.LineString([start - offset, end + offset])) - - unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines)) - closed_polygons = shapely.polygonize(unioned_boundaries.geoms) - - x, y = context.scene.cursor.location.xy - - space_polygon = None - for polygon in closed_polygons.geoms: - if shapely.contains_xy(polygon, x, y): - space_polygon = shapely.force_3d(polygon) - - if not space_polygon: - return - - bm = bmesh.new() - bm.verts.index_update() - bm.edges.index_update() - - new_verts = [bm.verts.new(v) for v in space_polygon.exterior.coords[0:-1]] - [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)] - bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0])) - - for interior in space_polygon.interiors: - new_verts = [bm.verts.new(v) for v in interior.coords[0:-1]] - [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)] - bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0])) - - bm.verts.index_update() - bm.edges.index_update() - - bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5) - bmesh.ops.triangle_fill(bm, edges=bm.edges) - bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges) - - extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces) - extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)] - bmesh.ops.translate(bm, vec=[0.0, 0.0, 3.0], verts=extruded_verts) - - mesh = bpy.data.meshes.new(name="Space") - bm.to_mesh(mesh) - bm.free() - - obj = bpy.data.objects.new("Space", mesh) - bpy.context.scene.collection.objects.link(obj) - - def get_wall_axis(self, obj): - x_values = [v[0] for v in obj.bound_box] - y_values = [v[1] for v in obj.bound_box] - min_x = min(x_values) - max_x = max(x_values) - min_y = min(y_values) - max_y = max(y_values) - return { - "miny": [ - (obj.matrix_world @ Vector((min_x, min_y, 0.0))).to_2d(), - (obj.matrix_world @ Vector((max_x, min_y, 0.0))).to_2d(), - ], - "maxy": [ - (obj.matrix_world @ Vector((min_x, max_y, 0.0))).to_2d(), - (obj.matrix_world @ Vector((max_x, max_y, 0.0))).to_2d(), - ], - } - - class GenerateHippedRoof(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.generate_hipped_roof" bl_label = "Generate Hipped Roof" diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index b520207246..fa47b68287 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -55,14 +55,6 @@ def get_type_predefined_type(self, context): return AuthoringData.data["type_predefined_type"] -def store_cursor_position(context, event, cursor=True, window=True): - browser_state = context.scene.BIMModelProperties.constr_browser_state - if cursor: - browser_state.cursor_x, browser_state.cursor_y = event.mouse_x, event.mouse_y - if window: - browser_state.window_x, browser_state.window_y = event.mouse_x, event.mouse_y - - def update_icon_id(self, context, browser=False): if context == "lost_context" or (context.region is not None and context.region.type != "TOOL_HEADER"): ifc_class = self.ifc_class_browser if browser else self.ifc_class diff --git a/src/blenderbim/blenderbim/bim/module/model/space.py b/src/blenderbim/blenderbim/bim/module/model/space.py new file mode 100644 index 0000000000..2e3e1813cc --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/model/space.py @@ -0,0 +1,156 @@ +# 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 bpy +import bmesh +import shapely +import ifcopenshell +import ifcopenshell.util.element +import blenderbim.tool as tool +import blenderbim.core.type +from math import pi +from mathutils import Vector, Matrix + + +class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.generate_space" + bl_label = "Generate Space" + bl_options = {"REGISTER"} + + def _execute(self, context): + # This only works based on a 2D plan only considering the standard + # layered walls (i.e. prismatic) in the currently active storey (though + # we can easily extend it to include other "bounding" elements). For + # now we generate rooms that exclude walls (i.e. not to wall midpoint + # or exterior / interior edge). + + props = context.scene.BIMModelProperties + relating_type_id = props.relating_type_id + relating_type = None + if relating_type_id: + relating_type = tool.Ifc.get().by_id(int(relating_type_id)) + if not relating_type.is_a("IfcSpaceType"): + relating_type = None + + collection = context.view_layer.active_layer_collection.collection + collection_obj = bpy.data.objects.get(collection.name) + if not collection_obj: + return + spatial_element = tool.Ifc.get_entity(collection_obj) + if not spatial_element: + return + + boundary_elements = [] + for subelement in ifcopenshell.util.element.get_decomposition(spatial_element): + if subelement.is_a("IfcWall") and tool.Model.get_usage_type(subelement) == "LAYER2": + boundary_elements.append(subelement) + + boundary_lines = [] + for element in boundary_elements: + obj = tool.Ifc.get_object(element) + if not obj: + continue + axis = self.get_wall_axis(obj) + for ypos in ["miny", "maxy"]: + start, end = axis[ypos] + offset = (end - start).normalized() * 0.3 # Offset wall lines by 300mm + boundary_lines.append(shapely.LineString([start - offset, end + offset])) + + unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines)) + closed_polygons = shapely.polygonize(unioned_boundaries.geoms) + + x, y = context.scene.cursor.location.xy + + space_polygon = None + for polygon in closed_polygons.geoms: + if shapely.contains_xy(polygon, x, y): + space_polygon = shapely.force_3d(polygon) + + if not space_polygon: + return + + bm = bmesh.new() + bm.verts.index_update() + bm.edges.index_update() + + new_verts = [bm.verts.new(v) for v in space_polygon.exterior.coords[0:-1]] + [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)] + bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0])) + + for interior in space_polygon.interiors: + new_verts = [bm.verts.new(v) for v in interior.coords[0:-1]] + [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)] + bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0])) + + bm.verts.index_update() + bm.edges.index_update() + + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5) + bmesh.ops.triangle_fill(bm, edges=bm.edges) + bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges) + + extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces) + extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)] + bmesh.ops.translate(bm, vec=[0.0, 0.0, 3.0], verts=extruded_verts) + + mesh = bpy.data.meshes.new(name="Space") + bm.to_mesh(mesh) + bm.free() + + active_obj = bpy.context.active_object + element = None + if bpy.context.selected_objects and active_obj: + element = tool.Ifc.get_entity(active_obj) + + if element and element.is_a("IfcSpace"): + mesh.name = active_obj.data.name + mesh.BIMMeshProperties.ifc_definition_id = active_obj.data.BIMMeshProperties.ifc_definition_id + tool.Geometry.change_object_data(active_obj, mesh, is_global=True) + tool.Ifc.edit(active_obj) + else: + if relating_type: + name = tool.Model.generate_occurrence_name(relating_type, "IfcSpace") + else: + name = "Space" + obj = bpy.data.objects.new("Space", mesh) + collection.objects.link(obj) + bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace") + element = tool.Ifc.get_entity(obj) + if relating_type: + print(element) + print(relating_type) + blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type) + + def get_wall_axis(self, obj): + x_values = [v[0] for v in obj.bound_box] + y_values = [v[1] for v in obj.bound_box] + min_x = min(x_values) + max_x = max(x_values) + min_y = min(y_values) + max_y = max(y_values) + return { + "miny": [ + (obj.matrix_world @ Vector((min_x, min_y, 0.0))).to_2d(), + (obj.matrix_world @ Vector((max_x, min_y, 0.0))).to_2d(), + ], + "maxy": [ + (obj.matrix_world @ Vector((min_x, max_y, 0.0))).to_2d(), + (obj.matrix_world @ Vector((max_x, max_y, 0.0))).to_2d(), + ], + } diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index e7f90de964..8964f9e52e 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -20,7 +20,7 @@ import bpy import blenderbim.tool as tool from bpy.types import Panel, Operator, Menu from blenderbim.bim.module.model.data import AuthoringData, ArrayData, StairData, SverchokData, WindowData, DoorData -from blenderbim.bim.module.model.prop import store_cursor_position, get_ifc_class +from blenderbim.bim.module.model.prop import get_ifc_class from blenderbim.bim.module.model.stair import update_stair_modifier from blenderbim.bim.module.model.window import update_window_modifier_bmesh from blenderbim.bim.module.model.door import update_door_modifier_bmesh @@ -125,86 +125,6 @@ class BIM_PT_authoring(Panel): row.operator("bim.align_wall", icon="ANCHOR_BOTTOM", text="Int.").align_type = "INTERIOR" -class DisplayConstrTypesUI(Operator): - bl_idname = "bim.display_constr_types_ui" - bl_label = "Browse Construction Types" - bl_options = {"REGISTER"} - bl_description = "Display all available Construction Types to add new instances" - reinvoked: bpy.props.BoolProperty(default=False) - - def execute(self, context): - return {"FINISHED"} - - def invoke(self, context, event): - store_cursor_position(context, event, window=not self.reinvoked) - return context.window_manager.invoke_popup(self, width=550) - - def reinvoke(self, context): - browser_state = context.scene.BIMModelProperties.constr_browser_state - - def set_updating_transaction(): - browser_state.updating = True - - def run_operator(): - bpy.ops.bim.reinvoke_operator("INVOKE_DEFAULT", operator="bim.display_constr_types_ui") - browser_state.updating = False - - if not browser_state.updating: - bpy.app.timers.register(set_updating_transaction) - bpy.app.timers.register(run_operator, first_interval=browser_state.update_delay) - - def draw(self, context): - props = context.scene.BIMModelProperties - - if AuthoringData.data["ifc_classes"]: - row = self.layout.row() - row.label(text="", icon="FILE_VOLUME") - prop_with_search(row, props, "ifc_class_browser", text="") - ifc_class = props.ifc_class_browser - num_cols = 3 - self.layout.row().separator(factor=0.25) - flow = self.layout.grid_flow(row_major=True, columns=num_cols, even_columns=True, even_rows=True, align=True) - relating_types = AuthoringData.relating_types_browser() - num_types = len(relating_types) - - for idx, (relating_type_id, name, desc) in enumerate(relating_types): - outer_col = flow.column() - box = outer_col.box() - row = box.row() - row.label(text=name, icon="FILE_3D") - row.alignment = "CENTER" - row = box.row() - - if ifc_class in props.constr_classes: - constr_class_info = props.constr_classes[ifc_class] - constr_types_info = constr_class_info.constr_types - - if relating_type_id in constr_types_info: - icon_id = constr_types_info[relating_type_id].icon_id - row.template_icon(icon_value=icon_id, scale=6.0) - else: - self.reinvoke(context) - return - - row = box.row() - op = row.operator("bim.add_constr_type_instance", icon="ADD") - op.from_invoke = True - op.ifc_class = ifc_class - - if relating_type_id.isnumeric(): - op.relating_type_id = int(relating_type_id) - - last_row_cols = num_types % num_cols - - if last_row_cols != 0: - for _ in range(num_cols - last_row_cols): - flow.column() - - row = self.layout.row() - row.alignment = "RIGHT" - row.operator("bim.help_relating_types", text="", icon="QUESTION") - - class HelpConstrTypes(Operator): bl_idname = "bim.help_relating_types" bl_label = "Construction Types Help" diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 0c77d65015..2b24a199f9 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -122,11 +122,6 @@ class BimToolUI: if not AuthoringData.is_loaded: AuthoringData.load() - # if ifc_classes and relating_types_ids and not cls.props.icon_id and not is_tool_header: - # # hack Dion won't like to show a preview also on the first time the sidebar is shown - # bpy.app.timers.register(lambda: prop.update_ifc_class(cls.props, "lost_context")) - # bpy.app.timers.register(lambda: prop.update_relating_type(cls.props, "lost_context")) - if context.region.type == "TOOL_HEADER": cls.draw_header_interface() elif context.region.type in ("UI", "WINDOW"): @@ -170,6 +165,11 @@ class BimToolUI: elif cls.props.ifc_class in ("IfcWindowType", "IfcWindowStyle", "IfcDoorType", "IfcDoorStyle"): row = cls.layout.row(align=True) row.prop(data=cls.props, property="rl2", text="RL") + elif cls.props.ifc_class in ("IfcSpaceType"): + row = cls.layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="", icon="EVENT_G") + add_layout_hotkey_operator(row, "Generate", "S_G", bpy.ops.bim.generate_space.__doc__) @classmethod def draw_edit_object_interface(cls, context): @@ -290,6 +290,11 @@ class BimToolUI: row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_F") row.operator("bim.hotkey", text="Flip").hotkey = "S_F" + elif AuthoringData.data["active_class"] in ("IfcSpace",): + row = cls.layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="", icon="EVENT_G") + add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__) elif AuthoringData.data["active_class"] in ( "IfcCableCarrierSegmentType", "IfcCableSegmentType", @@ -514,6 +519,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): def hotkey_S_G(self): if not bpy.context.selected_objects: + if self.props.ifc_class == "IfcSpaceType": + bpy.ops.bim.generate_space() return if self.active_material_usage == "LAYER2": bpy.ops.bim.recalculate_wall() @@ -521,6 +528,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.recalculate_profile() elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"): bpy.ops.bim.recalculate_fill() + elif self.active_class in ("IfcSpace"): + bpy.ops.bim.generate_space() def hotkey_S_M(self): if not bpy.context.selected_objects: diff --git a/src/blenderbim/blenderbim/tool/ifc.py b/src/blenderbim/blenderbim/tool/ifc.py index 7a85d77774..6d66459fff 100644 --- a/src/blenderbim/blenderbim/tool/ifc.py +++ b/src/blenderbim/blenderbim/tool/ifc.py @@ -88,6 +88,10 @@ class Ifc(blenderbim.core.tool.Ifc): def link(cls, element, obj): IfcStore.link_element(element, obj) + @classmethod + def edit(cls, obj): + IfcStore.edited_objs.add(obj) + @classmethod def delete(cls, element): IfcStore.delete_element(element)