Files
IfcOpenShell/src/bonsai/bonsai/tool/spatial.py
T
2024-08-14 16:49:34 +05:00

900 lines
37 KiB
Python

# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai 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.
#
# Bonsai 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 Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bmesh
import shapely
import shapely.ops
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import ifcopenshell.util.type
import ifcopenshell.util.unit
import bonsai.core.type
import bonsai.core.tool
import bonsai.core.root
import bonsai.core.spatial
import bonsai.core.geometry
import bonsai.tool as tool
import json
from math import pi
from mathutils import Vector, Matrix
from shapely import Polygon
from typing import Generator, Optional, Union, Literal, List
class Spatial(bonsai.core.tool.Spatial):
@classmethod
def can_contain(cls, container: ifcopenshell.entity_instance, element_obj: Union[bpy.types.Object, None]) -> bool:
element = tool.Ifc.get_entity(element_obj)
if not element:
return False
if tool.Ifc.get_schema() == "IFC2X3":
if not container.is_a("IfcSpatialStructureElement"):
return False
else:
if not container.is_a("IfcSpatialStructureElement") and not container.is_a(
"IfcExternalSpatialStructureElement"
):
return False
if not hasattr(element, "ContainedInStructure"):
return False
return True
@classmethod
def can_reference(
cls, structure: Union[ifcopenshell.entity_instance, None], element: Union[ifcopenshell.entity_instance, None]
) -> bool:
if not structure or not element:
return False
if tool.Ifc.get_schema() == "IFC2X3":
if not structure.is_a("IfcSpatialStructureElement"):
return False
else:
if not structure.is_a("IfcSpatialElement"):
return False
if not hasattr(element, "ReferencedInStructures"):
return False
return True
@classmethod
def disable_editing(cls, obj: bpy.types.Object) -> None:
obj.BIMObjectSpatialProperties.is_editing = False
@classmethod
def duplicate_object_and_data(cls, obj: bpy.types.Object) -> bpy.types.Object:
new_obj = obj.copy()
if obj.data:
new_obj.data = obj.data.copy()
return new_obj
@classmethod
def enable_editing(cls, obj: bpy.types.Object) -> None:
obj.BIMObjectSpatialProperties.is_editing = True
obj.BIMObjectSpatialProperties.relating_container_object = None
@classmethod
def get_container(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
return ifcopenshell.util.element.get_container(element)
@classmethod
def get_decomposed_elements(cls, container: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return ifcopenshell.util.element.get_decomposition(container)
@classmethod
def get_object_matrix(cls, obj: bpy.types.Object) -> Matrix:
return obj.matrix_world
@classmethod
def get_relative_object_matrix(cls, target_obj: bpy.types.Object, relative_to_obj: bpy.types.Object) -> Matrix:
return relative_to_obj.matrix_world.inverted() @ target_obj.matrix_world
@classmethod
def run_root_copy_class(cls, obj: bpy.types.Object) -> ifcopenshell.entity_instance:
return bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
@classmethod
def run_spatial_assign_container(
cls, container: ifcopenshell.entity_instance, element_obj: bpy.types.Object
) -> Union[ifcopenshell.entity_instance, None]:
return bonsai.core.spatial.assign_container(
tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj
)
@classmethod
def run_spatial_import_spatial_decomposition(cls) -> None:
return bonsai.core.spatial.import_spatial_decomposition(tool.Spatial)
@classmethod
def select_object(cls, obj: bpy.types.Object) -> None:
tool.Blender.select_object(obj)
@classmethod
def set_active_object(cls, obj: bpy.types.Object) -> None:
tool.Blender.set_active_object(obj)
@classmethod
def set_relative_object_matrix(
cls, target_obj: bpy.types.Object, relative_to_obj: bpy.types.Object, matrix: Matrix
) -> None:
target_obj.matrix_world = relative_to_obj.matrix_world @ matrix
@classmethod
def select_products(cls, products: list[ifcopenshell.entity_instance], unhide: bool = False) -> None:
bpy.ops.object.select_all(action="DESELECT")
for product in products:
obj = tool.Ifc.get_object(product)
if obj and bpy.context.view_layer.objects.get(obj.name):
if unhide:
obj.hide_set(False)
obj.select_set(True)
@classmethod
def filter_products(
cls, products: list[ifcopenshell.entity_instance], action: Literal["select", "isolate", "unhide", "hide"]
) -> None:
objects = [obj for product in products if (obj := tool.Ifc.get_object(product))]
if action == "select":
[obj.select_set(True) for obj in objects]
elif action == "isolate":
[obj.hide_set(False) for obj in objects if bpy.context.view_layer.objects.get(obj.name)]
[
obj.hide_set(True)
for obj in bpy.context.visible_objects
if not obj in objects and bpy.context.view_layer.objects.get(obj.name)
] # this is slow
elif action == "unhide":
[obj.hide_set(False) for obj in objects if bpy.context.view_layer.objects.get(obj.name)]
elif action == "hide":
[obj.hide_set(True) for obj in objects if bpy.context.view_layer.objects.get(obj.name)]
@classmethod
def deselect_objects(cls) -> None:
[obj.select_set(False) for obj in bpy.context.selected_objects]
@classmethod
def show_scene_objects(cls) -> None:
[
obj.hide_set(False)
for obj in bpy.data.scenes["Scene"].objects
if bpy.context.view_layer.objects.get(obj.name)
]
@classmethod
def get_selected_products(cls) -> Generator[ifcopenshell.entity_instance, None, None]:
for obj in bpy.context.selected_objects:
entity = tool.Ifc.get_entity(obj)
if entity and entity.is_a("IfcProduct"):
yield entity
@classmethod
def get_selected_product_types(cls) -> Generator[ifcopenshell.entity_instance, None, None]:
for obj in tool.Blender.get_selected_objects():
entity = tool.Ifc.get_entity(obj)
if entity and entity.is_a("IfcTypeProduct"):
yield entity
@classmethod
def copy_xy(cls, src_obj: bpy.types.Object, destination_obj: bpy.types.Object) -> None:
src_obj.location.xy = destination_obj.location.xy
@classmethod
def load_contained_elements(cls) -> None:
props = bpy.context.scene.BIMSpatialDecompositionProperties
props.elements.clear()
if not (container := props.active_container):
return
container = tool.Ifc.get().by_id(container.ifc_definition_id)
results = {}
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
else:
elements = ifcopenshell.util.element.get_contained(container)
for element in elements:
if not element.is_a("IfcElement"):
continue
element_type = ifcopenshell.util.element.get_type(element)
ifc_class = element.is_a()
ifc_definition_id = element_type.id() if element_type else 0
type_name = element_type.Name or "Unnamed" if element_type else "Untyped"
results.setdefault(ifc_class, {}).setdefault(ifc_definition_id, {"total": 0, "type_name": type_name})
results[ifc_class][ifc_definition_id]["total"] += 1
total_elements = 0
for ifc_class in sorted(results.keys()):
new = props.elements.add()
new.name = ifc_class
new.is_class = True
total = 0
for ifc_definition_id in sorted(
results[ifc_class].keys(), key=lambda x: results[ifc_class][x]["type_name"]
):
new2 = props.elements.add()
new2.is_type = True
new2.name = results[ifc_class][ifc_definition_id]["type_name"]
new2.total = results[ifc_class][ifc_definition_id]["total"]
new2.ifc_definition_id = ifc_definition_id
total += new2.total
new.total = total
total_elements += total
props.total_elements = total_elements
@classmethod
def filter_elements_by_class(cls, elements: List[ifcopenshell.entity_instance], ifc_class: str):
return [e for e in elements if e.is_a(ifc_class)]
@classmethod
def filter_elements_by_relating_type(
cls, elements: List[ifcopenshell.entity_instance], relating_type: ifcopenshell.entity_instance
):
return [e for e in elements if ifcopenshell.util.element.get_type(e) == relating_type]
@classmethod
def filter_elements_by_untyped(cls, elements: List[ifcopenshell.entity_instance]):
return [e for e in elements if not ifcopenshell.util.element.get_type(e)]
@classmethod
def import_spatial_decomposition(cls) -> None:
props = bpy.context.scene.BIMSpatialDecompositionProperties
previous_container_index = props.active_container_index
props.containers.clear()
cls.contracted_containers = json.loads(props.contracted_containers)
cls.import_spatial_element(tool.Ifc.get().by_type("IfcProject")[0], 0)
props.active_container_index = min(previous_container_index, len(props.containers) - 1)
@classmethod
def import_spatial_element(cls, element: ifcopenshell.entity_instance, level_index: int) -> None:
if not element.is_a("IfcProject") and not tool.Root.is_spatial_element(element):
return
props = bpy.context.scene.BIMSpatialDecompositionProperties
new = props.containers.add()
new.ifc_class = element.is_a()
new.name = element.Name or "Unnamed"
new.description = element.Description or ""
new.long_name = element.LongName or ""
if not element.is_a("IfcProject"):
ifc_file = tool.Ifc.get()
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
new.elevation = ifcopenshell.util.placement.get_storey_elevation(element) * si_conversion
new.is_expanded = element.id() not in cls.contracted_containers
new.level_index = level_index
children = ifcopenshell.util.element.get_parts(element)
new.has_children = bool(children)
new.ifc_definition_id = element.id()
if new.is_expanded:
for child in children or []:
cls.import_spatial_element(child, level_index + 1)
@classmethod
def edit_container_attributes(cls, entity: ifcopenshell.entity_instance) -> None:
# TODO
obj = tool.Ifc.get_object(entity)
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
name = bpy.context.scene.BIMSpatialDecompositionProperties.container_name
if name != entity.Name:
cls.edit_container_name(entity, name)
@classmethod
def edit_container_name(cls, container: ifcopenshell.entity_instance, name: str) -> None:
tool.Ifc.run("attribute.edit_attributes", product=container, attributes={"Name": name})
@classmethod
def get_active_container(cls) -> Union[ifcopenshell.entity_instance, None]:
props = bpy.context.scene.BIMSpatialDecompositionProperties
if props.active_container_index < len(props.containers):
container = tool.Ifc.get().by_id(props.containers[props.active_container_index].ifc_definition_id)
return container
@classmethod
def contract_container(cls, container: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMSpatialDecompositionProperties
contracted_containers = json.loads(props.contracted_containers)
contracted_containers.append(container.id())
props.contracted_containers = json.dumps(contracted_containers)
@classmethod
def expand_container(cls, container: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMSpatialDecompositionProperties
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 is_bounding_class(cls, visible_element: ifcopenshell.entity_instance) -> bool:
for ifc_class in ["IfcWall", "IfcColumn", "IfcMember", "IfcVirtualElement", "IfcPlate"]:
if visible_element.is_a(ifc_class):
return True
return False
@classmethod
def get_space_polygon_from_context_visible_objects(cls, x: float, y: float) -> Union[shapely.Polygon, None]:
boundary_lines = cls.get_boundary_lines_from_context_visible_objects()
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
space_polygon = None
for polygon in closed_polygons.geoms:
if shapely.contains_xy(polygon, x, y):
space_polygon = shapely.force_3d(polygon)
return space_polygon
@classmethod
def debug_shape(cls, foo: shapely.Polygon) -> None:
coords = [(p[0], p[1], 0) for p in foo.exterior.coords]
mesh = bpy.data.meshes.new(name="NewMesh")
bm = bmesh.new()
for coord in coords:
bm.verts.new(coord)
bm.verts.ensure_lookup_table()
bm.faces.new(bm.verts)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new("NewObject", mesh)
bpy.context.collection.objects.link(obj)
bpy.context.view_layer.update()
@classmethod
def debug_line(cls, start: Vector, end: Vector) -> None:
coords = [start, end]
mesh = bpy.data.meshes.new(name="NewMesh")
bm = bmesh.new()
for coord in coords:
bm.verts.new(coord)
bm.verts.ensure_lookup_table()
bm.edges.new(bm.verts)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new("NewLine", mesh)
bpy.context.collection.objects.link(obj)
bpy.context.view_layer.update()
@classmethod
def get_boundary_lines_from_context_visible_objects(cls) -> list[shapely.LineString]:
calculation_rl = bpy.context.scene.BIMModelProperties.rl3
container = tool.Root.get_default_container()
container_obj = tool.Ifc.get_object(container)
cut_point = container_obj.matrix_world.translation.copy() + Vector((0, 0, calculation_rl))
cut_normal = Vector((0, 0, 1))
boundary_lines = []
for obj in bpy.context.visible_objects:
visible_element = tool.Ifc.get_entity(obj)
if (
not visible_element
or obj.type != "MESH"
or not cls.is_bounding_class(visible_element)
or not tool.Drawing.is_intersecting_plane(obj, cut_point, cut_normal)
):
continue
old_mesh = obj.data
assert isinstance(old_mesh, bpy.types.Mesh)
if visible_element.HasOpenings:
new_mesh = cls.get_gross_mesh_from_element(visible_element)
else:
new_mesh = old_mesh.copy()
obj.data = new_mesh
# Boundary objects are likely triangulated. If a triangulated quad
# is bisected by our plane, we end up with two lines instead of
# one. This makes shapely's job much harder (since shapely is very
# exact with its coordinates). As a result, let's limited dissolve
# prior to bisecting.
bm = bmesh.new()
bm.from_mesh(new_mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=0.02, verts=bm.verts, edges=bm.edges)
bm.to_mesh(new_mesh)
new_mesh.update()
bm.free()
local_cut_point = obj.matrix_world.inverted() @ cut_point
local_cut_normal = obj.matrix_world.inverted().to_quaternion() @ cut_normal
verts, edges = tool.Drawing.bisect_mesh_with_plane(obj, local_cut_point, local_cut_normal)
# Restore the original mesh
obj.data = old_mesh
bpy.data.meshes.remove(new_mesh)
for edge in edges or []:
# Rounding is necessary to ensure coincident points are coincident
start = [round(x, 3) for x in verts[edge[0]]]
end = [round(x, 3) for x in verts[edge[1]]]
if start == end:
continue
# Extension by 50mm is necessary to ensure lines overlap with other diagonal lines
# This also closes small but likely irrelevant gaps for space generation.
start, end = tool.Drawing.extend_line(start, end, 0.05)
boundary_lines.append(shapely.LineString([start, end]))
return boundary_lines
@classmethod
def get_gross_mesh_from_element(cls, visible_element: ifcopenshell.entity_instance) -> bpy.types.Mesh:
gross_settings = ifcopenshell.geom.settings()
gross_settings.set("disable-opening-subtractions", True)
new_mesh = cls.create_mesh_from_shape(ifcopenshell.geom.create_shape(gross_settings, visible_element))
return new_mesh
@classmethod
def create_mesh_from_shape(cls, shape: ifcopenshell.geom.ShapeElementType) -> bpy.types.Mesh:
geometry = shape.geometry
mesh = bpy.data.meshes.new("tmp")
verts = geometry.verts
if geometry.faces:
num_vertices = len(verts) // 3
total_faces = len(geometry.faces)
loop_start = range(0, total_faces, 3)
num_loops = total_faces // 3
loop_total = [3] * num_loops
num_vertex_indices = len(geometry.faces)
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
mesh.loops.add(num_vertex_indices)
mesh.loops.foreach_set("vertex_index", geometry.faces)
mesh.polygons.add(num_loops)
mesh.polygons.foreach_set("loop_start", loop_start)
mesh.polygons.foreach_set("loop_total", loop_total)
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
mesh.update()
return mesh
@classmethod
def get_x_y_z_h_mat_from_obj(cls, obj: bpy.types.Object) -> tuple[float, float, float, float, Matrix]:
mat = obj.matrix_world
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
global_bbox_center = mat @ local_bbox_center
x = global_bbox_center.x
y = global_bbox_center.y
z = (mat @ Vector(obj.bound_box[0])).z
h = obj.dimensions.z
return x, y, z, h, mat
@classmethod
def get_x_y_z_h_mat_from_cursor(cls) -> tuple[float, float, float, float, Matrix]:
x, y, z = bpy.context.scene.cursor.location.xyz
if container := tool.Root.get_default_container():
if container_obj := tool.Ifc.get_object(container):
z = container_obj.matrix_world.translation.z
mat = Matrix()
h = 3
return x, y, z, h, mat
@classmethod
def get_union_shape_from_selected_objects(cls) -> Polygon:
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_si=0.03)
union = shapely.ops.unary_union(polys).buffer(
converted_tolerance,
cap_style=shapely.constructive.BufferCapStyle.flat,
join_style=shapely.constructive.BufferJoinStyle.mitre,
)
union = cls.get_purged_inner_holes_poly(
union_geom=union, min_area=cls.get_converted_tolerance(tolerance_si=0.1)
)
return union
@classmethod
def get_boundary_elements(cls, selected_objects: list[bpy.types.Object]) -> list[ifcopenshell.entity_instance]:
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: list[ifcopenshell.entity_instance]) -> list[Polygon]:
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: bpy.types.Object) -> dict[str, tuple[float, float]]:
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bbox_ws = [obj.matrix_world @ Vector(v) / si_conversion for v in obj.bound_box]
return {
"low_left": (bbox_ws[0].x, bbox_ws[0].y),
"high_left": (bbox_ws[3].x, bbox_ws[3].y),
"low_right": (bbox_ws[4].x, bbox_ws[4].y),
"high_right": (bbox_ws[7].x, bbox_ws[7].y),
}
@classmethod
def get_converted_tolerance(cls, tolerance_si: float) -> float:
model = tool.Ifc.get()
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(model)
return tolerance_si / si_conversion
@classmethod
def get_purged_inner_holes_poly(cls, union_geom: Polygon, min_area: float) -> Polygon:
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: Polygon, min_area: float, interiors_list: list[shapely.LinearRing]):
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: shapely.LinearRing) -> Polygon:
poly = Polygon(linear_ring)
converted_tolerance = cls.get_converted_tolerance(tolerance_si=0.03)
poly = poly.buffer(
converted_tolerance,
single_sided=True,
cap_style=shapely.BufferCapStyle.flat,
join_style=shapely.BufferJoinStyle.mitre,
)
return poly
@classmethod
def get_bmesh_from_polygon(cls, poly: Polygon, h: float) -> bmesh.types.BMesh:
mat = Matrix()
bm = bmesh.new()
bm.verts.index_update()
bm.edges.index_update()
mat_invert = mat.inverted()
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
new_verts = [
bm.verts.new(mat_invert @ (Vector([v[0], v[1], 0]) * si_conversion)) 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: str, bmesh: bmesh.types.BMesh) -> bpy.types.Object:
mesh = cls.get_named_mesh_from_bmesh(name, bmesh)
obj = cls.get_named_obj_from_mesh(name, mesh)
return obj
@classmethod
def get_named_obj_from_mesh(cls, name: str, mesh: bpy.types.Mesh) -> bpy.types.Object:
obj = bpy.data.objects.new(name, mesh)
return obj
@classmethod
def get_named_mesh_from_bmesh(cls, name: str, bmesh: bmesh.types.BMesh) -> bpy.types.Mesh:
mesh = bpy.data.meshes.new(name=name)
bmesh.to_mesh(mesh)
bmesh.free()
return mesh
@classmethod
def get_transformed_mesh_from_local_to_global(cls, mesh: bpy.types.Mesh) -> bpy.types.Mesh:
active_obj = cls.get_active_obj()
mat = active_obj.matrix_world
mesh.transform(mat.inverted())
mesh.update()
return mesh
@classmethod
def edit_active_space_obj_from_mesh(cls, mesh: bpy.types.Mesh) -> None:
active_obj = bpy.context.active_object
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)
@classmethod
def set_obj_origin_to_bboxcenter(cls, obj: bpy.types.Object) -> None:
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 set_obj_origin_to_bboxcenter_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
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
global_obj_origin = global_bbox_center
global_obj_origin.z = 0
oldLoc = obj.location
newLoc = global_obj_origin
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 set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
mat = obj.matrix_world
inverted = mat.inverted()
x, y = bpy.context.scene.cursor.location.xy
z = 0
oldLoc = obj.location
newLoc = Vector((x, y, z))
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_selected_objects(cls) -> list[bpy.types.Object]:
return bpy.context.selected_objects
@classmethod
def get_active_obj(cls) -> Union[bpy.types.Object, None]:
return bpy.context.active_object
@classmethod
def get_active_obj_z(cls) -> float:
return bpy.context.active_object.matrix_world.translation.z
@classmethod
def get_active_obj_height(cls) -> float:
height = bpy.context.active_object.dimensions.z
return height
@classmethod
def get_relating_type_id(cls) -> int:
props = bpy.context.scene.BIMModelProperties
relating_type_id = props.relating_type_id
return relating_type_id
@classmethod
def translate_obj_to_z_location(cls, obj: bpy.types.Object, z: float) -> None:
if z != 0:
obj.location = obj.location + Vector((0, 0, z))
@classmethod
def get_2d_vertices_from_obj(cls, obj: bpy.types.Object) -> list[tuple]:
points = []
vectors = [v.co for v in obj.data.vertices.values()]
for vector in vectors:
points.append(vector.xy)
points.append(vectors[0].xy)
return points
@classmethod
def get_scaled_2d_vertices(cls, points: list[Vector]) -> list[tuple[float, float]]:
model = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(model)
_points = []
for p in points:
_p = list(p)
_p[0] /= unit_scale
_p[1] /= unit_scale
_points.append(_p)
return _points
@classmethod
def assign_swept_area_outer_curve_from_2d_vertices(cls, obj: bpy.types.Object, vertices: list[Vector]) -> None:
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: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
element = tool.Ifc.get_entity(obj)
return ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
@classmethod
def assign_ifcspace_class_to_obj(cls, obj: bpy.types.Object) -> None:
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
@classmethod
def assign_type_to_obj(cls, obj: bpy.types.Object) -> None:
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)
tool.Ifc.run("type.assign_type", related_objects=[element], relating_type=relating_type)
@classmethod
def assign_relating_type_to_element(
cls,
ifc: tool.Ifc,
type: tool.Type,
element: ifcopenshell.entity_instance,
relating_type: ifcopenshell.entity_instance,
) -> None:
bonsai.core.type.assign_type(ifc, type, element=element, type=relating_type)
@classmethod
def regen_obj_representation(cls, obj: bpy.types.Object, body: ifcopenshell.entity_instance) -> None:
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.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: list[ifcopenshell.entity_instance]) -> None:
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
@classmethod
def toggle_hide_spaces(cls, spaces: list[ifcopenshell.entity_instance]) -> None:
first_obj = tool.Ifc.get_object(spaces[0])
if first_obj.hide_get() == False:
for space in spaces:
obj = tool.Ifc.get_object(space)
obj.hide_set(True)
return
elif first_obj.hide_get() == True:
for space in spaces:
obj = tool.Ifc.get_object(space)
obj.hide_set(False)
@classmethod
def set_default_container(cls, container: ifcopenshell.entity_instance) -> None:
bpy.context.scene.BIMSpatialDecompositionProperties.default_container = container.id()
project = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
project_collection = project.BIMObjectProperties.collection
obj = tool.Ifc.get_object(container)
if obj and (collection := obj.BIMObjectProperties.collection):
for layer_collection in bpy.context.view_layer.layer_collection.children:
if layer_collection.collection == project_collection:
for layer_collection2 in layer_collection.children:
if layer_collection2.collection == collection:
bpy.context.view_layer.active_layer_collection = layer_collection2
break
@classmethod
def guess_default_container(cls) -> Optional[ifcopenshell.entity_instance]:
project = tool.Ifc.get().by_type("IfcProject")[0]
subelement = None
# We try to priorise the first Site > Building > Storey as a convention for vertical projects
for subelement in ifcopenshell.util.element.get_parts(project):
if subelement.is_a("IfcSite"):
for subelement2 in ifcopenshell.util.element.get_parts(subelement):
if subelement2.is_a("IfcBuilding"):
for subelement3 in ifcopenshell.util.element.get_parts(subelement2):
if subelement3.is_a("IfcBuildingStorey"):
return subelement3
if subelement:
return subelement
return None
@classmethod
def get_selected_containers(cls) -> List[ifcopenshell.entity_instance]:
results = []
for obj in tool.Blender.get_selected_objects():
if (element := tool.Ifc.get_entity(obj)) and tool.Root.is_spatial_element(element):
results.append(element)
return results
@classmethod
def get_selected_objects_without_containers(cls) -> list[bpy.types.Object]:
"""Get selected objects skipping spatial elements.
Useful for operators that are using selected objects to identify selected containers.
Note that those operators are typically have a limitation since they can't tell
objects to operate on from containers that should be used in the operation.
E.g. we cannot bim.copy_to_container containers to other containers."""
results: list[bpy.types.Object] = []
for obj in tool.Blender.get_selected_objects():
if (element := tool.Ifc.get_entity(obj)) and not tool.Root.is_spatial_element(element):
results.append(obj)
return results
@classmethod
def set_target_container_as_default(cls) -> None:
if (
(container := tool.Root.get_default_container())
and (obj := tool.Ifc.get_object(container))
and bpy.context.active_object
):
props = bpy.context.active_object.BIMObjectSpatialProperties
props.container_obj = obj