Merge pull request #3790 from maxfb87/new_covering_tools

New covering tools (see #3754)
This commit is contained in:
Massimo Fabbro
2023-09-25 16:03:56 +02:00
committed by GitHub
18 changed files with 804 additions and 201 deletions
@@ -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,
@@ -0,0 +1,41 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,40 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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")
@@ -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 <http://www.gnu.org/licenses/>.
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()
@@ -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,
@@ -0,0 +1,64 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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)
@@ -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
@@ -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"}
@@ -0,0 +1,42 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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)
+61
View File
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
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)
+22
View File
@@ -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
@@ -0,0 +1,49 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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
+255 -1
View File
@@ -17,13 +17,18 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
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
Binary file not shown.
+1
View File
@@ -7,6 +7,7 @@ markers =
classification
context
cost
covering
debug
demo
document
@@ -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
@@ -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