diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 9528ab8869..ccafb12784 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -78,6 +78,7 @@ modules = { "augin": None, "debug": None, "ifcgit": None, + "covering": None, # Uncomment this line to enable loading of the demo module. Happy hacking! # The name "demo" must correlate to a folder name in `bim/module/`. # "demo": None, diff --git a/src/blenderbim/blenderbim/bim/module/covering/__init__.py b/src/blenderbim/blenderbim/bim/module/covering/__init__.py new file mode 100644 index 0000000000..73c39a93af --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/covering/__init__.py @@ -0,0 +1,41 @@ +# 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 bpy +from . import prop, workspace + +classes = ( + prop.BIMCoveringProperties, + workspace.Hotkey, +) + + +def register(): + if not bpy.app.background: + bpy.utils.register_tool(workspace.CoveringTool, after={"bim.structural_tool"}, separator=False, group=False) + bpy.types.Scene.BIMCoveringProperties = bpy.props.PointerProperty(type=prop.BIMCoveringProperties) +# bpy.types.Object.BIMObjectSpatialProperties = bpy.props.PointerProperty(type=prop.BIMObjectSpatialProperties) +# bpy.types.Scene.BIMSpatialManagerProperties = bpy.props.PointerProperty(type=prop.BIMSpatialManagerProperties) + + +def unregister(): + if not bpy.app.background: + bpy.utils.unregister_tool(workspace.CoveringTool) + del bpy.types.Scene.BIMCoveringProperties +# del bpy.types.Object.BIMObjectSpatialProperties +# del bpy.types.Scene.BIMSpatialManagerProperties diff --git a/src/blenderbim/blenderbim/bim/module/covering/ops.authoring.covering.dat b/src/blenderbim/blenderbim/bim/module/covering/ops.authoring.covering.dat new file mode 100644 index 0000000000..8a860d3149 Binary files /dev/null and b/src/blenderbim/blenderbim/bim/module/covering/ops.authoring.covering.dat differ diff --git a/src/blenderbim/blenderbim/bim/module/covering/prop.py b/src/blenderbim/blenderbim/bim/module/covering/prop.py new file mode 100644 index 0000000000..d66aac26cd --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/covering/prop.py @@ -0,0 +1,40 @@ +# 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 bpy +from blenderbim.bim.prop import StrProperty, Attribute +#from blenderbim.bim.module.spatial.data import SpatialData +from bpy.types import PropertyGroup +from bpy.props import ( + PointerProperty, + StringProperty, + EnumProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + CollectionProperty, +) +import blenderbim.tool as tool +import blenderbim.core.geometry +import ifcopenshell + + +class BIMCoveringProperties(PropertyGroup): + pass +# depth: bpy.props.FloatProperty(name="Depth", default=0.1, subtype="DISTANCE", description="Flooring depth") diff --git a/src/blenderbim/blenderbim/bim/module/covering/workspace.py b/src/blenderbim/blenderbim/bim/module/covering/workspace.py new file mode 100644 index 0000000000..336d3bc2ef --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/covering/workspace.py @@ -0,0 +1,166 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2023 @Andrej730 +# +# 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 os +import bpy +import blenderbim.tool as tool +from blenderbim.bim.helper import prop_with_search +from blenderbim.bim.module.model.data import AuthoringData +from bpy.types import WorkSpaceTool +from blenderbim.bim.ifc import IfcStore +import blenderbim.bim.handler + + +# declaring it here to avoid circular import problems +class Operator: + def execute(self, context): + IfcStore.execute_ifc_operator(self, context) + blenderbim.bim.handler.refresh_ui_data() + return {"FINISHED"} + +class CoveringTool(WorkSpaceTool): + bl_space_type = "VIEW_3D" + bl_context_mode = "OBJECT" + bl_idname = "bim.covering_tool" + bl_label = "Covering Tool" + bl_description = "Create and edit coverings" + bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.covering") + bl_widget = None + bl_keymap = tool.Blender.get_default_selection_keypmap() + ( + ("bim.covering_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}), + ) + + @classmethod + def draw_settings(cls, context, layout, ws_tool): + CoveringToolUI.draw(context, layout, ifc_element_type="IfcCoveringType") + +def add_layout_hotkey(layout, text, hotkey, description): + args = ["covering", layout, text, hotkey, description] + tool.Blender.add_layout_hotkey_operator(*args) + +class CoveringToolUI: + @classmethod + def draw(cls, context, layout, ifc_element_type = None): + cls.layout = layout + cls.props = context.scene.BIMModelProperties + cls.covering_props = context.scene.BIMCoveringProperties + + row = cls.layout.row(align=True) + if not tool.Ifc.get(): + row.label(text="No IFC Project", icon="ERROR") + return + + if not AuthoringData.is_loaded: + AuthoringData.load(ifc_element_type) + elif AuthoringData.data["ifc_element_type"] != ifc_element_type: + AuthoringData.load(ifc_element_type) + + + if context.region.type == "TOOL_HEADER": + cls.draw_header_interface() + elif context.region.type in ("UI", "WINDOW"): + cls.draw_basic_bim_tool_interface() + + cls.draw_default_interface() + + + + @classmethod + def draw_header_interface(cls): + cls.draw_type_selection_interface() + + @classmethod + def draw_default_interface(cls): + if AuthoringData.data["ifc_classes"]: + row = cls.layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="", icon="EVENT_A") + active_obj = bpy.context.active_object + element = tool.Ifc.get_entity(active_obj) + if element and bpy.context.selected_objects and element.is_a("IfcWall"): + op = row.operator("bim.add_instance_flooring_coverings_from_walls") + else: + op = row.operator("bim.add_constr_type_instance", text="Add") + op.from_invoke = True + if cls.props.relating_type_id.isnumeric(): + op.relating_type_id = int(cls.props.relating_type_id) + + @classmethod + def draw_type_selection_interface(cls): + # shared by both sidebar and header + row = cls.layout.row(align=True) + if AuthoringData.data["ifc_classes"]: + row = cls.layout.row(align=True) + row.label(text="", icon="FILE_3D") + prop_with_search(row, cls.props, "relating_type_id", text="") + row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="") + else: + row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR") + row = cls.layout.row() + row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="Launch Type Manager") + + @classmethod + def draw_basic_bim_tool_interface(cls): + cls.draw_type_selection_interface() + + if AuthoringData.data["ifc_classes"]: + if cls.props.ifc_class: + box = cls.layout.box() + if AuthoringData.data["type_thumbnail"]: + box.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=5) + else: + op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH") + op.ifc_class = cls.props.ifc_class + + +class Hotkey(bpy.types.Operator, Operator): + bl_idname = "bim.covering_hotkey" + bl_label = "Hotkey" + bl_options = {"REGISTER", "UNDO"} + hotkey: bpy.props.StringProperty() + description: bpy.props.StringProperty() + + @classmethod + def poll(cls, context): + return tool.Ifc.get() + + @classmethod + def description(cls, context, operator): + return operator.description or "" + + def _execute(self, context): +# self.props = context.scene.BIMCoveringProperties + getattr(self, f"hotkey_{self.hotkey}")() + + def invoke(self, context, event): + # https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey + # self.props = context.scene.BIMSpatialProperties + return self.execute(context) + + def draw(self, context): + pass + + def hotkey_S_A(self): + active_obj = bpy.context.active_object + element = tool.Ifc.get_entity(active_obj) + if element and bpy.context.selected_objects and element.is_a("IfcWall"): + bpy.ops.bim.add_instance_flooring_coverings_from_walls() + else: + bpy.ops.bim.add_constr_type_instance() + diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index bdb5f5fe01..7786a0d8e5 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -28,6 +28,7 @@ from . import ( roof, slab, space, + covering, stair, window, opening, @@ -106,6 +107,7 @@ classes = ( slab.SetArcIndex, space.GenerateSpace, space.GenerateSpacesFromWalls, + covering.AddInstanceFlooringCoveringsFromWalls, space.ToggleSpaceVisibility, mep.FitFlowSegments, mep.RegenerateDistributionElement, diff --git a/src/blenderbim/blenderbim/bim/module/model/covering.py b/src/blenderbim/blenderbim/bim/module/model/covering.py new file mode 100644 index 0000000000..da5f09cb06 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/model/covering.py @@ -0,0 +1,64 @@ +# 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 ifcopenshell +import blenderbim.tool as tool +import blenderbim.core.covering as core + + +class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_instance_flooring_coverings_from_walls" + bl_label = "Add Typed Covering From Walls" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Add instance flooring coverings from selected walls. The active object must be a wall and layered vertically" + + @classmethod + def poll(cls, context): + active_obj = bpy.context.active_object + element = tool.Ifc.get_entity(active_obj) + if element: + if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2": + return context.selected_objects + + def _execute(self, context): + # This only works based on a 2D plan only considering the standard + # walls (i.e. prismatic) in the active object storey. + # In order to run, the active object must be a wall and + # there must be selected walls + + active_obj = bpy.context.active_object + if not active_obj: + self.report({"ERROR"}, "No active object. Please select a wall") + return + + element = tool.Ifc.get_entity(active_obj) + if element and not element.is_a("IfcWall"): + return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.") + + container = ifcopenshell.util.element.get_container(element) + if not container: + self.report({"ERROR"}, "The wall is not contained.") + + if not bpy.context.selected_objects: + self.report({"ERROR"}, "No selected objects found. Please select walls.") + return + + core.add_instance_flooring_coverings_from_walls(tool.Ifc, tool.Spatial, tool.Collector, tool.Geometry) + diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index ab2534cfa8..2723e7904e 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -132,7 +132,7 @@ class BIMModelProperties(PropertyGroup): y: bpy.props.FloatProperty(name="Y", default=0.5, subtype="DISTANCE", description="Size by Y axis for the opening") z: bpy.props.FloatProperty(name="Z", default=0.5, subtype="DISTANCE", description="Size by Z axis for the opening") # Used for things like walls, doors, flooring, skirting, etc - rl1: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for walls") + rl1: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for walls") # Used for things like windows, other hosted furniture, and MEP rl2: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for windows") # Used for plan calculation points such as in room generation diff --git a/src/blenderbim/blenderbim/bim/module/model/space.py b/src/blenderbim/blenderbim/bim/module/model/space.py index 2af8ae020c..5f0e8edaf3 100644 --- a/src/blenderbim/blenderbim/bim/module/model/space.py +++ b/src/blenderbim/blenderbim/bim/module/model/space.py @@ -23,6 +23,7 @@ import shapely import ifcopenshell import ifcopenshell.util.element import blenderbim.tool as tool +import blenderbim.core.spatial as core import blenderbim.core.type from math import pi from mathutils import Vector, Matrix @@ -230,182 +231,9 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): # This only works based on a 2D plan only considering the standard # walls (i.e. prismatic) in the active object storey. - # In order to run, the active objct must be a wall and - # have to be selected walls - props = context.scene.BIMModelProperties - active_obj = bpy.context.active_object - - if not active_obj: - self.report({"ERROR"}, "No active object. Please select a wall") - return - - element = tool.Ifc.get_entity(active_obj) - if element and not element.is_a("IfcWall"): - return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.") - - container = ifcopenshell.util.element.get_container(element) - if not container: - self.report({"ERROR"}, "The wall is not contained.") - - if not bpy.context.selected_objects: - self.report({"ERROR"}, "No selected objects found. Please select walls.") - return - - x, y, z = active_obj.matrix_world.translation.xyz - mat = active_obj.matrix_world - h = active_obj.dimensions.z - selected_objects = bpy.context.selected_objects - - boundary_elements = self.get_boundary_elements(selected_objects) - - polys = self.get_polygons(boundary_elements) - - converted_tolerance = self.get_converted_tolerance(tolerance=0.03) - - union = shapely.ops.unary_union(polys).buffer(converted_tolerance, cap_style=2, join_style=2) - - union = self.get_purged_inner_holes_poly(union_geom=union, min_area=self.get_converted_tolerance(tolerance=3)) - - for i, linear_ring in enumerate(union.interiors): - poly = Polygon(linear_ring) - poly = poly.buffer(converted_tolerance, single_sided=True, cap_style=2, join_style=2) - - bm = self.get_bmesh_from_polygon(poly, mat, h) - - name = "Space" + str(i) - mesh = bpy.data.meshes.new(name=name) - bm.to_mesh(mesh) - bm.free() - - obj = bpy.data.objects.new(name, mesh) - obj.matrix_world = mat - - self.set_obj_origin_to_bboxcenter(obj) - - if z != 0: - obj.location = obj.location + Vector((0, 0, z)) - - context.view_layer.active_layer_collection.collection.objects.link(obj) - bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace") - container_obj = tool.Ifc.get_object(container) - blenderbim.core.spatial.assign_container( - tool.Ifc, tool.Collector, tool.Spatial, structure_obj=container_obj, element_obj=obj - ) - - def get_boundary_elements(self, selected_objects): - boundary_elements = [] - for obj in selected_objects: - subelement = tool.Ifc.get_entity(obj) - if subelement.is_a("IfcWall") or subelement.is_a("IfcColumn"): - boundary_elements.append(subelement) - return boundary_elements - - def get_polygons(self, boundary_elements): - polys = [] - for boundary_element in boundary_elements: - obj = tool.Ifc.get_object(boundary_element) - if not obj: - continue - points = [] - base = self.get_obj_base_points(obj) - for index in ["low_left", "low_right", "high_right", "high_left"]: - point = base[index] - points.append(point) - - polys.append(Polygon(points)) - return polys - - def get_obj_base_points(self, obj): - x_values = [(obj.matrix_world @ Vector(v)).x for v in obj.bound_box] - y_values = [(obj.matrix_world @ Vector(v)).y for v in obj.bound_box] - return { - "low_left": (x_values[0], y_values[0]), - "high_left": (x_values[3], y_values[3]), - "low_right": (x_values[4], y_values[4]), - "high_right": (x_values[7], y_values[7]), - } - - def get_converted_tolerance(self, tolerance): - model = tool.Ifc.get() - project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT") - prefix = getattr(project_unit, "Prefix", None) - - converted_tolerance = ifcopenshell.util.unit.convert( - value=tolerance, - from_prefix=None, - from_unit="METRE", - to_prefix=prefix, - to_unit=project_unit.Name, - ) - return tolerance - - def get_purged_inner_holes_poly(self, union_geom, min_area): - interiors_list = [] - - if union_geom.geom_type == "MultiPolygon": - for poly in union_geom.geoms: - interiors_list = self.get_poly_valid_interior_list( - poly=poly, min_area=min_area, interiors_list=interiors_list - ) - - new_poly = Polygon(poly.exterior.coords, holes=interiors_list) - - if union_geom.geom_type == "Polygon": - interiors_list = self.get_poly_valid_interior_list( - poly=union_geom, min_area=min_area, interiors_list=interiors_list - ) - new_poly = Polygon(union_geom.exterior.coords, holes=interiors_list) - - return new_poly - - def get_poly_valid_interior_list(self, poly, min_area, interiors_list): - for interior in poly.interiors: - p = Polygon(interior) - if p.area >= min_area: - interiors_list.append(interior) - return interiors_list - - def get_bmesh_from_polygon(self, poly, mat, h): - bm = bmesh.new() - bm.verts.index_update() - bm.edges.index_update() - - mat_invert = mat.inverted() - - new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], 0])) for v in poly.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])) - - 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, h], verts=extruded_verts) - - bmesh.ops.recalc_face_normals(bm, faces=bm.faces) - - return bm - - def set_obj_origin_to_bboxcenter(self, obj): - mat = obj.matrix_world - inverted = mat.inverted() - local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector()) - global_bbox_center = mat @ local_bbox_center - - oldLoc = obj.location - newLoc = global_bbox_center - diff = newLoc - oldLoc - for vert in obj.data.vertices: - aux_vector = mat @ vert.co - aux_vector = aux_vector - diff - vert.co = inverted @ aux_vector - obj.location = newLoc - + # In order to run, the active object must be a wall and + # there must be selected walls + core.generate_spaces_from_walls(tool.Ifc, tool.Spatial, tool.Collector) class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.toggle_space_visibility" @@ -414,26 +242,6 @@ class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Change the space visibility" def execute(cls, context): - model = tool.Ifc.get() + core.toggle_space_visibility(tool.Ifc, tool.Spatial) + return {"FINISHED"} - spaces = model.by_type("IfcSpace") - - if not spaces: - print(spaces) - return {"FINISHED"} - - first_obj = tool.Ifc.get_object(spaces[0]) - - if bpy.data.objects[first_obj.name].display_type == "TEXTURED": - for space in spaces: - obj = tool.Ifc.get_object(space) - bpy.data.objects[obj.name].show_wire = True - bpy.data.objects[obj.name].display_type = "WIRE" - return {"FINISHED"} - - elif bpy.data.objects[first_obj.name].display_type == "WIRE": - for space in spaces: - obj = tool.Ifc.get_object(space) - bpy.data.objects[obj.name].show_wire = False - bpy.data.objects[obj.name].display_type = "TEXTURED" - return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/core/covering.py b/src/blenderbim/blenderbim/core/covering.py new file mode 100644 index 0000000000..61aadfcd7a --- /dev/null +++ b/src/blenderbim/blenderbim/core/covering.py @@ -0,0 +1,42 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 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 . + + +def add_instance_flooring_coverings_from_walls(ifc, spatial, collector, geometry): + z = spatial.get_active_obj_z() + union = spatial.get_union_shape_from_selected_objects() + for i, linear_ring in enumerate(union.interiors): + poly = spatial.get_buffered_poly_from_linear_ring(linear_ring) + bm = spatial.get_bmesh_from_polygon(poly, h=0) + + name = "Covering" + str(i) + obj = spatial.get_named_obj_from_bmesh(name, bmesh = bm) + + spatial.set_obj_origin_to_bboxcenter(obj) + spatial.traslate_obj_to_z_location(obj, z) + spatial.link_obj_to_active_collection(obj) + + points = spatial.get_2d_vertices_from_obj(obj) + + spatial.assign_type_to_obj(obj) + spatial.assign_container_to_obj(obj) + + spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices = points) + body = spatial.get_body_representation(obj) + spatial.regen_obj_representation(ifc, geometry, obj, body) + diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py index fb75dd2671..7f2beb3aba 100644 --- a/src/blenderbim/blenderbim/core/spatial.py +++ b/src/blenderbim/blenderbim/core/spatial.py @@ -17,6 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import blenderbim.core +import bpy def reference_structure(ifc, spatial, structure=None, element=None): @@ -120,3 +121,63 @@ def select_decomposed_elements(spatial): container = spatial.get_active_container() if container: spatial.select_products(spatial.get_decomposed_elements(container)) + +#HERE STARTS SPATIAL TOOL + +def generate_spaces_from_walls(ifc, spatial, collector): + container, active_obj = spatial.get_container_and_active_obj() + + if not active_obj: + self.report({"ERROR"}, "No active object. Please select a wall") + return + + element = ifc.get_entity(active_obj) + if element and not element.is_a("IfcWall"): + return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.") + + if not container: + self.report({"ERROR"}, "The wall is not contained.") + + if not bpy.context.selected_objects: + self.report({"ERROR"}, "No selected objects found. Please select walls.") + return + + x, y, z = active_obj.matrix_world.translation.xyz + mat = active_obj.matrix_world + h = active_obj.dimensions.z + selected_objects = bpy.context.selected_objects + + union = spatial.get_union_shape_from_selected_objects(selected_objects) + + for i, linear_ring in enumerate(union.interiors): + poly = spatial.get_buffered_poly_from_linear_ring(linear_ring) + + bm = spatial.get_bmesh_from_polygon(poly, mat, h) + + name = "Space" + str(i) + mesh = bpy.data.meshes.new(name=name) + bm.to_mesh(mesh) + bm.free() + + obj = bpy.data.objects.new(name, mesh) + obj.matrix_world = mat + + spatial.set_obj_origin_to_bboxcenter(obj) + + if z != 0: + obj.location = obj.location + Vector((0, 0, z)) + + bpy.context.view_layer.active_layer_collection.collection.objects.link(obj) + bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace") + container_obj = ifc.get_object(container) + blenderbim.core.spatial.assign_container( + ifc, collector, spatial, structure_obj=container_obj, element_obj=obj + ) + +def toggle_space_visibility(ifc, spatial): + model = ifc.get() + spaces = model.by_type("IfcSpace") + if not spaces: + return + spatial.toggle_spaces_visibility_wired_and_textured(spaces) + diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 7e769df9dc..0557e0144d 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -809,6 +809,28 @@ class Spatial: def set_active_object(cls, obj): pass def set_relative_object_matrix(cls, target_obj, relative_to_obj, matrix): pass def show_scene_objects(cls): pass + #HERE STARTS SPATIAL TOOL +# def get_container_and_active_obj(cls): pass + def get_union_shape_from_selected_objects(cls, selected_objects): pass + def get_boundary_elements(cls, selected_objects): pass + def get_polygons(cls, boundary_elements): pass + def get_obj_base_points(cls, obj): pass + def get_converted_tolerance(cls, tolerance): pass + def get_purged_inner_holes_poly(cls, union_geom, min_area): pass + def get_poly_valid_interior_list(cls, poly, min_area, interiors_list): pass + def get_buffered_poly_from_linear_ring(cls, linear_ring): pass + def get_bmesh_from_polygon(cls, poly, h): pass + def get_named_obj_from_bmesh(cls, name, bmesh): pass + def set_obj_origin_to_bboxcenter(cls, obj): pass + def get_active_obj_z(cls, obj): pass + def traslate_obj_to_z_location(cls, obj): pass + def link_obj_to_active_collection(cls, obj): pass + def get_2d_vertices_from_obj(cls, obj): pass + def assign_swept_area_outer_curve_from_2d_vertices(cls, obj, vertices): pass + def get_body_representation(cls, obj): pass + def assign_type_to_obj(cls, obj): pass + def regen_obj_representation(cls, ifc, geometry, obj, body): pass + def toggle_spaces_visibility_wired_and_textured(cls, spaces): pass @interface diff --git a/src/blenderbim/blenderbim/tool/covering.py b/src/blenderbim/blenderbim/tool/covering.py new file mode 100644 index 0000000000..cfbae2f807 --- /dev/null +++ b/src/blenderbim/blenderbim/tool/covering.py @@ -0,0 +1,49 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 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 bpy +import bmesh +import shapely +import ifcopenshell +import blenderbim.core.tool +import blenderbim.core.root +import blenderbim.core.spatial +import blenderbim.core.geometry +import blenderbim.tool as tool +import json +from math import pi +from mathutils import Vector, Matrix +from shapely import Polygon, MultiPolygon + +class Covering(blenderbim.core.tool.Covering): + @classmethod +# def toggle_spaces_visibility_wired_and_textured(cls, spaces): +# first_obj = tool.Ifc.get_object(spaces[0]) +# if bpy.data.objects[first_obj.name].display_type == "TEXTURED": +# for space in spaces: +# obj = tool.Ifc.get_object(space) +# bpy.data.objects[obj.name].show_wire = True +# bpy.data.objects[obj.name].display_type = "WIRE" +# return +# +# elif bpy.data.objects[first_obj.name].display_type == "WIRE": +# for space in spaces: +# obj = tool.Ifc.get_object(space) +# bpy.data.objects[obj.name].show_wire = False +# bpy.data.objects[obj.name].display_type = "TEXTURED" +# return diff --git a/src/blenderbim/blenderbim/tool/spatial.py b/src/blenderbim/blenderbim/tool/spatial.py index 5c3b6b895a..f838703059 100644 --- a/src/blenderbim/blenderbim/tool/spatial.py +++ b/src/blenderbim/blenderbim/tool/spatial.py @@ -17,13 +17,18 @@ # along with BlenderBIM Add-on. If not, see . import bpy +import bmesh +import shapely import ifcopenshell import blenderbim.core.tool import blenderbim.core.root import blenderbim.core.spatial +import blenderbim.core.geometry import blenderbim.tool as tool import json - +from math import pi +from mathutils import Vector, Matrix +from shapely import Polygon, MultiPolygon class Spatial(blenderbim.core.tool.Spatial): @classmethod @@ -262,3 +267,252 @@ class Spatial(blenderbim.core.tool.Spatial): contracted_containers = json.loads(props.contracted_containers) contracted_containers.remove(container.id()) props.contracted_containers = json.dumps(contracted_containers) + +#HERE STARTS SPATIAL TOOL + + @classmethod + def get_union_shape_from_selected_objects(cls): + selected_objects = bpy.context.selected_objects + boundary_elements = cls.get_boundary_elements(selected_objects) + polys = cls.get_polygons(boundary_elements) + converted_tolerance = cls.get_converted_tolerance(tolerance=0.03) + union = shapely.ops.unary_union(polys).buffer(converted_tolerance, cap_style=2, join_style=2) + union = cls.get_purged_inner_holes_poly(union_geom=union, min_area=cls.get_converted_tolerance(tolerance=3)) + + return union + + @classmethod + def get_boundary_elements(cls, selected_objects): + boundary_elements = [] + for obj in selected_objects: + subelement = tool.Ifc.get_entity(obj) + if subelement.is_a("IfcWall") or subelement.is_a("IfcColumn"): + boundary_elements.append(subelement) + return boundary_elements + + @classmethod + def get_polygons(cls, boundary_elements): + polys = [] + for boundary_element in boundary_elements: + obj = tool.Ifc.get_object(boundary_element) + if not obj: + continue + points = [] + base = cls.get_obj_base_points(obj) + for index in ["low_left", "low_right", "high_right", "high_left"]: + point = base[index] + points.append(point) + + polys.append(Polygon(points)) + return polys + + @classmethod + def get_obj_base_points(cls, obj): + x_values = [(obj.matrix_world @ Vector(v)).x for v in obj.bound_box] + y_values = [(obj.matrix_world @ Vector(v)).y for v in obj.bound_box] + return { + "low_left": (x_values[0], y_values[0]), + "high_left": (x_values[3], y_values[3]), + "low_right": (x_values[4], y_values[4]), + "high_right": (x_values[7], y_values[7]), + } + + @classmethod + def get_converted_tolerance(cls, tolerance): + model = tool.Ifc.get() + project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT") + prefix = getattr(project_unit, "Prefix", None) + + converted_tolerance = ifcopenshell.util.unit.convert( + value=tolerance, + from_prefix=None, + from_unit="METRE", + to_prefix=prefix, + to_unit=project_unit.Name, + ) + return tolerance + + @classmethod + def get_purged_inner_holes_poly(cls, union_geom, min_area): + interiors_list = [] + + if union_geom.geom_type == "MultiPolygon": + for poly in union_geom.geoms: + interiors_list = cls.get_poly_valid_interior_list( + poly=poly, min_area=min_area, interiors_list=interiors_list + ) + + new_poly = Polygon(poly.exterior.coords, holes=interiors_list) + + if union_geom.geom_type == "Polygon": + interiors_list = cls.get_poly_valid_interior_list( + poly=union_geom, min_area=min_area, interiors_list=interiors_list + ) + new_poly = Polygon(union_geom.exterior.coords, holes=interiors_list) + + return new_poly + + @classmethod + def get_poly_valid_interior_list(cls, poly, min_area, interiors_list): + for interior in poly.interiors: + p = Polygon(interior) + if p.area >= min_area: + interiors_list.append(interior) + return interiors_list + + @classmethod + def get_buffered_poly_from_linear_ring(cls, linear_ring): + poly = Polygon(linear_ring) + converted_tolerance = cls.get_converted_tolerance(tolerance=0.03) + poly = poly.buffer(converted_tolerance, single_sided=True, cap_style=2, join_style=2) + return poly + + @classmethod + def get_bmesh_from_polygon(cls, poly, h): + mat = bpy.context.active_object.matrix_world + bm = bmesh.new() + bm.verts.index_update() + bm.edges.index_update() + + mat_invert = mat.inverted() + + new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], 0])) for v in poly.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])) + + 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) + + if h!=0: + 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, h], verts=extruded_verts) + + bmesh.ops.recalc_face_normals(bm, faces=bm.faces) + + return bm + + @classmethod + def get_named_obj_from_bmesh(cls, name, bmesh): + mesh = bpy.data.meshes.new(name=name) + bmesh.to_mesh(mesh) + bmesh.free() + obj = bpy.data.objects.new(name, mesh) + mat = bpy.context.active_object.matrix_world + obj.matrix_world = mat + return obj + + @classmethod + def set_obj_origin_to_bboxcenter(cls, obj): + mat = obj.matrix_world + inverted = mat.inverted() + local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector()) + global_bbox_center = mat @ local_bbox_center + + oldLoc = obj.location + newLoc = global_bbox_center + diff = newLoc - oldLoc + for vert in obj.data.vertices: + aux_vector = mat @ vert.co + aux_vector = aux_vector - diff + vert.co = inverted @ aux_vector + obj.location = newLoc + + @classmethod + def get_active_obj_z(cls): + x, y, z = bpy.context.active_object.matrix_world.translation.xyz + return z + + @classmethod + def traslate_obj_to_z_location(cls, obj, z): + if z != 0: + obj.location = obj.location + Vector((0, 0, z)) + + @classmethod + def link_obj_to_active_collection(cls, obj): + bpy.context.view_layer.active_layer_collection.collection.objects.link(obj) + + @classmethod + def get_2d_vertices_from_obj(cls, obj): + points = [] + vectors = [v.co for v in obj.data.vertices.values()] + for vector in vectors: + point = (vector[0], vector[1]) + points.append(point) + + points.append((vectors[0][0], vectors[0][1])) + return points + + @classmethod + def assign_swept_area_outer_curve_from_2d_vertices(cls, obj, vertices): + body = cls.get_body_representation(obj) + model = tool.Ifc.get() + extrusion = tool.Model.get_extrusion(body) + area = extrusion.SweptArea + old_area = area.OuterCurve + + builder = ifcopenshell.util.shape_builder.ShapeBuilder(model) + outer_curve = builder.polyline(vertices, closed = True) + + area.OuterCurve = outer_curve + ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_area) + + @classmethod + def get_body_representation(cls, obj): + element = tool.Ifc.get_entity(obj) + model = tool.Ifc.get() + body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + return body + + @classmethod + def assign_type_to_obj(cls, obj): + relating_type_id = bpy.context.scene.BIMModelProperties.relating_type_id + relating_type = tool.Ifc.get().by_id(int(relating_type_id)) + ifc_class = relating_type.is_a() + instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, tool.Ifc.get().schema)[0] + bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class) + element = tool.Ifc.get_entity(obj) + blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type) + + @classmethod + def assign_container_to_obj(cls, obj): + active_obj = bpy.context.active_object + element = tool.Ifc.get_entity(active_obj) + container = ifcopenshell.util.element.get_container(element) + container_obj = tool.Ifc.get_object(container) + blenderbim.core.spatial.assign_container( + tool.Ifc, tool.Collector, tool.Spatial, structure_obj=container_obj, element_obj=obj + ) + + @classmethod + def regen_obj_representation(cls, ifc, geometry, obj, body): + blenderbim.core.geometry.switch_representation( + ifc, + geometry, + obj=obj, + representation=body, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) + + @classmethod + def toggle_spaces_visibility_wired_and_textured(cls, spaces): + first_obj = tool.Ifc.get_object(spaces[0]) + if bpy.data.objects[first_obj.name].display_type == "TEXTURED": + for space in spaces: + obj = tool.Ifc.get_object(space) + bpy.data.objects[obj.name].show_wire = True + bpy.data.objects[obj.name].display_type = "WIRE" + return + + elif bpy.data.objects[first_obj.name].display_type == "WIRE": + for space in spaces: + obj = tool.Ifc.get_object(space) + bpy.data.objects[obj.name].show_wire = False + bpy.data.objects[obj.name].display_type = "TEXTURED" + return diff --git a/src/blenderbim/blenderbim_icons.blend b/src/blenderbim/blenderbim_icons.blend index df16936ee6..387e6815f1 100644 Binary files a/src/blenderbim/blenderbim_icons.blend and b/src/blenderbim/blenderbim_icons.blend differ diff --git a/src/blenderbim/pytest.ini b/src/blenderbim/pytest.ini index 4b4d9ae52c..2f55059a87 100644 --- a/src/blenderbim/pytest.ini +++ b/src/blenderbim/pytest.ini @@ -7,6 +7,7 @@ markers = classification context cost + covering debug demo document diff --git a/src/blenderbim/test/bim/feature/covering.feature b/src/blenderbim/test/bim/feature/covering.feature new file mode 100644 index 0000000000..bb7bdaf45d --- /dev/null +++ b/src/blenderbim/test/bim/feature/covering.feature @@ -0,0 +1,15 @@ +@covering +Feature: Covering + Covers covering tool. + +Scenario: Execute generate flooring coverings from walls + Given an empty IFC project + And I load the demo construction library + And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" + And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" + And I press "bim.add_constr_type_instance" + And the object "IfcWall/Wall" is selected + When I press "bim.add_instance_flooring_coverings_from_walls" + Then nothing happens + diff --git a/src/blenderbim/test/bim/feature/spatial.feature b/src/blenderbim/test/bim/feature/spatial.feature index 8262e9d0cb..03e54ef479 100644 --- a/src/blenderbim/test/bim/feature/spatial.feature +++ b/src/blenderbim/test/bim/feature/spatial.feature @@ -1,6 +1,6 @@ @spatial Feature: Spatial - Covers spatial containment management. + Covers spatial containment management and spatial tool. Scenario: Enable editing container Given an empty IFC project @@ -97,3 +97,40 @@ Scenario: Select similar container And I press "bim.assign_container(structure={site})" When I press "bim.select_similar_container" Then nothing happens + +#HERE STARTS TESTS FOR SPATIAL TOOL + +Scenario: Execute generate space from cursor position + Given an empty IFC project + When I press "bim.generate_space" + Then nothing happens + +Scenario: Execute generate spaces from walls + Given an empty IFC project + And I load the demo construction library + And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" + And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" + And I press "bim.add_constr_type_instance" + And the object "IfcWall/Wall" is selected + When I press "bim.generate_spaces_from_walls" + Then nothing happens + +Scenario: Execute generate flooring coverings from walls + Given an empty IFC project + And I load the demo construction library + And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" + And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" + And I press "bim.add_constr_type_instance" + And the object "IfcWall/Wall" is selected + When I press "bim.generate_flooring_coverings_from_walls" + Then nothing happens + +Scenario: Execute toggle space visibility + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I press "bim.assign_class(ifc_class='IfcSpace', predefined_type='SPACE')" + When I press "bim.toggle_space_visibility" + Then nothing happens