mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-19 11:43:53 +00:00
1003 lines
41 KiB
Python
1003 lines
41 KiB
Python
# 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 struct
|
|
import hashlib
|
|
import logging
|
|
import numpy as np
|
|
import ifcopenshell
|
|
import ifcopenshell.api
|
|
import ifcopenshell.geom
|
|
import ifcopenshell.guid
|
|
import ifcopenshell.util.element
|
|
import ifcopenshell.util.representation
|
|
import ifcopenshell.util.system
|
|
import blenderbim.core.tool
|
|
import blenderbim.core.drawing
|
|
import blenderbim.core.style
|
|
import blenderbim.core.spatial
|
|
import blenderbim.core.system
|
|
import blenderbim.core.geometry
|
|
import blenderbim.tool as tool
|
|
import blenderbim.bim.import_ifc
|
|
from math import radians, pi
|
|
from mathutils import Vector, Matrix
|
|
from blenderbim.bim.ifc import IfcStore
|
|
from typing import Union, Iterable, Optional
|
|
|
|
|
|
class Geometry(blenderbim.core.tool.Geometry):
|
|
@classmethod
|
|
def change_object_data(cls, obj: bpy.types.Object, data: bpy.types.ID, is_global: bool = False) -> None:
|
|
if is_global:
|
|
cls.replace_object_data_globally(obj.data, data)
|
|
else:
|
|
obj.data = data
|
|
|
|
@classmethod
|
|
def replace_object_data_globally(cls, old_data: bpy.types.ID, new_data: bpy.types.ID) -> None:
|
|
if getattr(old_data, "is_editmode", None):
|
|
raise Exception("user_remap is not supported for meshes in EDIT mode")
|
|
old_data.user_remap(new_data)
|
|
|
|
@classmethod
|
|
def clear_cache(cls, element):
|
|
cache = IfcStore.get_cache()
|
|
if cache and hasattr(element, "GlobalId"):
|
|
cache.remove(element.GlobalId)
|
|
|
|
@classmethod
|
|
def clear_modifiers(cls, obj):
|
|
for modifier in obj.modifiers:
|
|
obj.modifiers.remove(modifier)
|
|
|
|
@classmethod
|
|
def clear_scale(cls, obj):
|
|
# Note that clearing scale has no impact on cameras.
|
|
if (obj.scale - Vector((1.0, 1.0, 1.0))).length > 1e-4:
|
|
if not obj.data:
|
|
location, rotation, _ = obj.matrix_world.decompose()
|
|
obj.matrix_world = Matrix.Translation(location) @ rotation.to_matrix().to_4x4()
|
|
obj.matrix_world.normalize()
|
|
elif obj.data.users == 1:
|
|
context_override = {}
|
|
context_override["object"] = context_override["active_object"] = obj
|
|
context_override["selected_objects"] = context_override["selected_editable_objects"] = [obj]
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
|
else:
|
|
obj.scale = Vector((1.0, 1.0, 1.0))
|
|
|
|
@classmethod
|
|
def delete_data(cls, data):
|
|
bpy.data.meshes.remove(data)
|
|
|
|
@classmethod
|
|
def delete_ifc_object(cls, obj: bpy.types.Object) -> None:
|
|
element = tool.Ifc.get_entity(obj)
|
|
if not element:
|
|
return
|
|
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
|
return blenderbim.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
|
|
if element.is_a("IfcRelSpaceBoundary"):
|
|
ifcopenshell.api.run("boundary.remove_boundary", tool.Ifc.get(), boundary=element)
|
|
return bpy.data.objects.remove(obj)
|
|
if element.is_a("IfcGridAxis"):
|
|
is_last_axis = False
|
|
# Deleting the last W axis is OK
|
|
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
|
|
(grid := element.PartOfV) and len(grid[0].VAxes) == 1
|
|
):
|
|
is_last_axis = True
|
|
if is_last_axis:
|
|
return
|
|
ifcopenshell.api.run("grid.remove_grid_axis", tool.Ifc.get(), axis=element)
|
|
return bpy.data.objects.remove(obj)
|
|
|
|
collection = obj.BIMObjectProperties.collection
|
|
if collection:
|
|
parent = ifcopenshell.util.element.get_aggregate(element)
|
|
if not parent:
|
|
parent = ifcopenshell.util.element.get_container(element)
|
|
if parent:
|
|
parent_obj = tool.Ifc.get_object(parent)
|
|
if parent_obj:
|
|
parent_collection = parent_obj.BIMObjectProperties.collection
|
|
for child in collection.children:
|
|
parent_collection.children.link(child)
|
|
for child_object in collection.objects:
|
|
parent_collection.objects.link(child_object)
|
|
bpy.data.collections.remove(collection)
|
|
if getattr(element, "FillsVoids", None):
|
|
bpy.ops.bim.remove_filling(filling=element.id())
|
|
|
|
if element.is_a("IfcOpeningElement"):
|
|
if element.HasFillings:
|
|
for rel in element.HasFillings:
|
|
bpy.ops.bim.remove_filling(filling=rel.RelatedBuildingElement.id())
|
|
else:
|
|
if element.VoidsElements:
|
|
bpy.ops.bim.remove_opening(opening_id=element.id())
|
|
else:
|
|
is_spatial = element.is_a("IfcSpatialElement") or element.is_a("IfcSpatialStructureElement")
|
|
if getattr(element, "HasOpenings", None):
|
|
for rel in element.HasOpenings:
|
|
bpy.ops.bim.remove_opening(opening_id=rel.RelatedOpeningElement.id())
|
|
for port in ifcopenshell.util.system.get_ports(element):
|
|
blenderbim.core.system.remove_port(tool.Ifc, tool.System, port=port)
|
|
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
|
|
|
|
if isinstance(obj.data, bpy.types.Mesh) and not tool.Ifc.get_entity_by_id(
|
|
obj.data.BIMMeshProperties.ifc_definition_id
|
|
):
|
|
tool.Blender.remove_data_block(obj.data)
|
|
|
|
if is_spatial:
|
|
blenderbim.core.spatial.import_spatial_decomposition(tool.Spatial)
|
|
try:
|
|
obj.name
|
|
bpy.data.objects.remove(obj)
|
|
except:
|
|
pass
|
|
|
|
@classmethod
|
|
def dissolve_triangulated_edges(cls, obj):
|
|
if obj.data and "ios_edges" in obj.data:
|
|
bm = bmesh.new()
|
|
bm.from_mesh(obj.data)
|
|
edges_to_keep = set(map(frozenset, obj.data["ios_edges"]))
|
|
edges_to_dissolve = []
|
|
for edge in bm.edges:
|
|
if frozenset([vert.index for vert in edge.verts]) not in edges_to_keep:
|
|
edges_to_dissolve.append(edge)
|
|
bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve)
|
|
bm.to_mesh(obj.data)
|
|
bm.free()
|
|
del obj.data["ios_edges"]
|
|
|
|
@classmethod
|
|
def does_representation_id_exist(cls, representation_id):
|
|
try:
|
|
tool.Ifc.get().by_id(representation_id)
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
@classmethod
|
|
def duplicate_object_data(cls, obj):
|
|
if obj.data:
|
|
return obj.data.copy()
|
|
|
|
@classmethod
|
|
def generate_2d_box_mesh(cls, obj, axis="Z"):
|
|
bm = bmesh.new()
|
|
verts = [Vector(corner) for corner in obj.bound_box]
|
|
if axis == "Z":
|
|
verts = [verts[i] for i in [0, 4, 7, 3]]
|
|
for v in verts:
|
|
v.z = 0
|
|
elif axis == "Y":
|
|
verts = [verts[i] for i in [0, 4, 5, 1]]
|
|
for v in verts:
|
|
v.y = 0
|
|
elif axis == "X":
|
|
verts = [verts[i] for i in [4, 7, 6, 5]]
|
|
for v in verts:
|
|
v.x = 0
|
|
bm.faces.new([bm.verts.new(v) for v in verts])
|
|
|
|
mesh = bpy.data.meshes.new(name="tmp")
|
|
bm.to_mesh(mesh)
|
|
bm.free()
|
|
return mesh
|
|
|
|
@classmethod
|
|
def generate_3d_box_mesh(cls, obj):
|
|
bm = bmesh.new()
|
|
verts = [bm.verts.new(Vector(corner)) for corner in obj.bound_box]
|
|
|
|
bm.faces.new([verts[i] for i in [0, 3, 7, 4]])
|
|
bm.faces.new([verts[i] for i in [0, 1, 2, 3]])
|
|
bm.faces.new([verts[i] for i in [0, 4, 5, 1]])
|
|
bm.faces.new([verts[i] for i in [4, 7, 6, 5]])
|
|
bm.faces.new([verts[i] for i in [7, 3, 2, 6]])
|
|
bm.faces.new([verts[i] for i in [1, 5, 6, 2]])
|
|
|
|
mesh = bpy.data.meshes.new(name="tmp")
|
|
bm.to_mesh(mesh)
|
|
bm.free()
|
|
return mesh
|
|
|
|
@classmethod
|
|
def generate_outline_mesh(cls, obj, axis="+Z"):
|
|
def get_visible_faces(obj, bm, axis="+Z"):
|
|
# A visible face is any face with the normal facing the axis and
|
|
# its centroid not obscured (tested via raycasting) by any other
|
|
# face.
|
|
distance = max(obj.dimensions.xyz)
|
|
if axis == "+Z":
|
|
max_z = max([co[2] for co in obj.bound_box]) + 0.002
|
|
direction = Vector((0, 0, -1))
|
|
elif axis == "-Y":
|
|
min_y = max([co[2] for co in obj.bound_box]) - 0.002
|
|
direction = Vector((0, 1, 0))
|
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
visible_faces = []
|
|
face_offset = obj.matrix_world.to_quaternion() @ Vector((0, 0, distance))
|
|
global_direction = obj.matrix_world.to_quaternion() @ direction
|
|
for face in bm.faces:
|
|
if direction.dot(face.normal) > 0:
|
|
continue
|
|
if axis == "+Z":
|
|
face_centroid_at_max = Vector((*face.calc_center_median().xy, max_z))
|
|
elif axis == "-Y":
|
|
centroid = face.calc_center_median()
|
|
face_centroid_at_max = Vector((centroid.x, min_y, centroid.z))
|
|
face_centroid_at_max = obj.matrix_world @ face_centroid_at_max
|
|
hit, loc, norm, idx, o, mw = bpy.context.scene.ray_cast(
|
|
depsgraph, face_centroid_at_max, global_direction, distance=distance
|
|
)
|
|
if o != obj or idx == face.index:
|
|
visible_faces.append(face)
|
|
return visible_faces
|
|
|
|
def get_contour_edges(visible_faces):
|
|
# A contour is any edge where one face is visible and the other isn't.
|
|
contour_edges = []
|
|
for face in visible_faces:
|
|
for edge in face.edges:
|
|
total_linked_faces = len(edge.link_faces)
|
|
if total_linked_faces == 1:
|
|
contour_edges.append(edge)
|
|
elif total_linked_faces == 2:
|
|
other_face = edge.link_faces[0] if edge.link_faces[1] == face else edge.link_faces[1]
|
|
if other_face not in visible_faces:
|
|
contour_edges.append(edge)
|
|
return contour_edges
|
|
|
|
def get_crease_edges(visible_faces, threshold):
|
|
# A crease is any edge with a face angle greater than a threshold.
|
|
crease_edges = []
|
|
for face in visible_faces:
|
|
for edge in face.edges:
|
|
if len(edge.link_faces) == 2:
|
|
angle = edge.link_faces[0].normal.angle(edge.link_faces[1].normal)
|
|
if abs(angle) > threshold:
|
|
crease_edges.append(edge)
|
|
return crease_edges
|
|
|
|
# Calculate outline edges
|
|
bm = bmesh.new()
|
|
bm.from_mesh(obj.data)
|
|
visible_faces = get_visible_faces(obj, bm, axis=axis)
|
|
outline_edges = set(get_contour_edges(visible_faces))
|
|
outline_edges.update(get_crease_edges(visible_faces, radians(60)))
|
|
|
|
# Copy outline edges to new bmesh
|
|
bm.to_mesh(obj.data)
|
|
bm_new = bmesh.new()
|
|
vert_map = {}
|
|
|
|
for edge in outline_edges:
|
|
verts = []
|
|
for vert in edge.verts:
|
|
if vert not in vert_map:
|
|
new_vert = bm_new.verts.new(vert.co)
|
|
vert_map[vert] = new_vert
|
|
verts.append(vert_map[vert])
|
|
bm_new.edges.new(verts)
|
|
|
|
# Flatten along axis in new bmesh
|
|
for vert in bm_new.verts:
|
|
if axis == "+Z":
|
|
vert.co.z = 0
|
|
elif axis == "-Y":
|
|
vert.co.y = 0
|
|
|
|
# Convert new bmesh to new mesh
|
|
new_mesh = bpy.data.meshes.new("tmp")
|
|
bm_new.to_mesh(new_mesh)
|
|
|
|
bm_new.free()
|
|
bm.free()
|
|
|
|
return new_mesh
|
|
|
|
@classmethod
|
|
def get_active_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
|
"""< IfcShapeRepresentation or None"""
|
|
if obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.ifc_definition_id:
|
|
return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
|
|
|
@classmethod
|
|
def get_active_representation_context(cls, obj):
|
|
active_representation = tool.Geometry.get_active_representation(obj)
|
|
if active_representation:
|
|
return active_representation.ContextOfItems
|
|
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
|
|
|
@classmethod
|
|
def get_subcontext_parameters(
|
|
cls, subcontext: ifcopenshell.entity_instance
|
|
) -> tuple[Union[str, None], Union[str, None], Union[str, None]]:
|
|
return (
|
|
subcontext.ContextType,
|
|
subcontext.ContextIdentifier,
|
|
getattr(subcontext, "TargetView", None),
|
|
)
|
|
|
|
@classmethod
|
|
def get_representation_by_context(cls, element, context):
|
|
if element.is_a("IfcProduct") and element.Representation:
|
|
for r in element.Representation.Representations:
|
|
if r.ContextOfItems == context:
|
|
return r
|
|
elif element.is_a("IfcTypeProduct") and element.RepresentationMaps:
|
|
for r in element.RepresentationMaps:
|
|
if r.MappedRepresentation.ContextOfItems == context:
|
|
return r.MappedRepresentation
|
|
|
|
@classmethod
|
|
def get_cartesian_point_coordinate_offset(cls, obj):
|
|
props = bpy.context.scene.BIMGeoreferenceProperties
|
|
if props.has_blender_offset and obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT":
|
|
return Vector(
|
|
(
|
|
float(props.blender_eastings),
|
|
float(props.blender_northings),
|
|
float(props.blender_orthogonal_height),
|
|
)
|
|
)
|
|
|
|
@classmethod
|
|
def get_element_type(cls, element):
|
|
return ifcopenshell.util.element.get_type(element)
|
|
|
|
@classmethod
|
|
def get_elements_of_type(cls, type):
|
|
return ifcopenshell.util.element.get_types(type)
|
|
|
|
@classmethod
|
|
def get_ifc_representation_class(cls, element, representation):
|
|
if element.is_a("IfcAnnotation"):
|
|
if element.ObjectType == "TEXT":
|
|
return "IfcTextLiteral"
|
|
elif element.ObjectType == "TEXT_LEADER":
|
|
return "IfcGeometricCurveSet/IfcTextLiteral"
|
|
|
|
material = ifcopenshell.util.element.get_material(element)
|
|
if material and material.is_a("IfcMaterialProfileSetUsage"):
|
|
return "IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage"
|
|
|
|
extruded_areas = [e for e in tool.Ifc.get().traverse(representation) if e.is_a() == "IfcExtrudedAreaSolid"]
|
|
|
|
if len(extruded_areas) != 1:
|
|
return # It's too complex for us to derive topologically right now
|
|
|
|
profile_def = extruded_areas[0].SweptArea
|
|
|
|
if profile_def.is_a() == "IfcRectangleProfileDef":
|
|
return "IfcExtrudedAreaSolid/IfcRectangleProfileDef"
|
|
elif profile_def.is_a() == "IfcCircleProfileDef":
|
|
return "IfcExtrudedAreaSolid/IfcCircleProfileDef"
|
|
return "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids"
|
|
|
|
@classmethod
|
|
def get_mesh_checksum(cls, mesh):
|
|
data_bytes = b""
|
|
if isinstance(mesh, bpy.types.Mesh):
|
|
vertices = mesh.vertices[:]
|
|
edges = mesh.edges[:]
|
|
faces = mesh.polygons[:]
|
|
|
|
# Convert mesh data to bytes
|
|
for v in vertices:
|
|
data_bytes += struct.pack("3f", *v.co)
|
|
for e in edges:
|
|
data_bytes += struct.pack("2i", *e.vertices)
|
|
for f in faces:
|
|
data_bytes += struct.pack("%di" % len(f.vertices), *f.vertices)
|
|
elif isinstance(mesh, bpy.types.Curve):
|
|
splines = mesh.splines[:]
|
|
|
|
for spline in splines:
|
|
if spline.type == "BEZIER":
|
|
for bezier_point in spline.bezier_points:
|
|
data_bytes += struct.pack("3f", *bezier_point.co)
|
|
data_bytes += struct.pack("3f", *bezier_point.handle_left)
|
|
data_bytes += struct.pack("3f", *bezier_point.handle_right)
|
|
else:
|
|
for point in spline.points:
|
|
data_bytes += struct.pack("4f", *point.co)
|
|
|
|
hasher = hashlib.sha1()
|
|
hasher.update(data_bytes)
|
|
return hasher.hexdigest()
|
|
|
|
@classmethod
|
|
def get_object_data(cls, obj):
|
|
return obj.data
|
|
|
|
@classmethod
|
|
def get_object_materials_without_styles(cls, obj):
|
|
return [
|
|
s.material for s in obj.material_slots if s.material and not s.material.BIMMaterialProperties.ifc_style_id
|
|
]
|
|
|
|
@classmethod
|
|
def get_profile_set_usage(cls, element):
|
|
material = ifcopenshell.util.element.get_material(element)
|
|
if material:
|
|
if material.is_a("IfcMaterialProfileSetUsage"):
|
|
return material
|
|
|
|
@classmethod
|
|
def get_representation_data(cls, representation):
|
|
return bpy.data.meshes.get(cls.get_representation_name(representation))
|
|
|
|
@classmethod
|
|
def get_representation_id(cls, representation):
|
|
return representation.id()
|
|
|
|
@classmethod
|
|
def get_representation_name(cls, representation):
|
|
return f"{representation.ContextOfItems.id()}/{representation.id()}"
|
|
|
|
@classmethod
|
|
def get_styles(
|
|
cls, obj: bpy.types.Object, only_assigned_to_faces: bool = False
|
|
) -> list[Union[ifcopenshell.entity_instance, None]]:
|
|
styles = [tool.Style.get_style(s.material) for s in obj.material_slots if s.material]
|
|
if not only_assigned_to_faces:
|
|
return styles
|
|
|
|
usage_count = [0] * len(obj.material_slots)
|
|
if not usage_count: # if there are no materials, polygons will still use index 0
|
|
return []
|
|
|
|
for poly in obj.data.polygons:
|
|
usage_count[poly.material_index] += 1
|
|
|
|
# remove usages for empty material slots
|
|
for i, slot in reversed(list(enumerate(obj.material_slots))):
|
|
if not slot.material:
|
|
del usage_count[i]
|
|
|
|
styles = [style for style, usage in zip(styles, usage_count, strict=True) if usage > 0]
|
|
return styles
|
|
|
|
# TODO: multiple Literals?
|
|
@classmethod
|
|
def get_text_literal(cls, representation):
|
|
texts = [i for i in representation.Items if i.is_a("IfcTextLiteral")]
|
|
if texts:
|
|
return texts[0]
|
|
|
|
@classmethod
|
|
def get_total_representation_items(cls, obj):
|
|
return max(1, len(obj.material_slots))
|
|
|
|
@classmethod
|
|
def has_data_users(cls, data):
|
|
return data.users != 0
|
|
|
|
@classmethod
|
|
def has_geometric_data(cls, obj):
|
|
if not obj.data:
|
|
return False
|
|
if isinstance(obj.data, bpy.types.Mesh):
|
|
return bool(obj.data.vertices)
|
|
elif isinstance(obj.data, bpy.types.Curve):
|
|
return bool(obj.data.splines)
|
|
return False
|
|
|
|
@classmethod
|
|
def import_representation(cls, obj, representation, apply_openings=True):
|
|
logger = logging.getLogger("ImportIFC")
|
|
ifc_import_settings = blenderbim.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger)
|
|
element = tool.Ifc.get_entity(obj)
|
|
settings = ifcopenshell.geom.settings()
|
|
settings.set("weld-vertices", True)
|
|
context = representation.ContextOfItems
|
|
|
|
if element.is_a("IfcTypeProduct"):
|
|
# You may only specify a single representation when creating shapes for types
|
|
try:
|
|
shape = ifcopenshell.geom.create_shape(settings, representation)
|
|
except:
|
|
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
|
shape = ifcopenshell.geom.create_shape(settings, representation)
|
|
else:
|
|
if not apply_openings:
|
|
settings.set("disable-opening-subtractions", True)
|
|
|
|
if context.ContextIdentifier == "Body" and context.TargetView == "MODEL_VIEW":
|
|
try:
|
|
shape = ifcopenshell.geom.create_shape(settings, element, representation)
|
|
except:
|
|
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
|
shape = ifcopenshell.geom.create_shape(settings, element, representation)
|
|
else:
|
|
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
|
shape = ifcopenshell.geom.create_shape(settings, element, representation)
|
|
|
|
ifc_importer = blenderbim.bim.import_ifc.IfcImporter(ifc_import_settings)
|
|
ifc_importer.file = tool.Ifc.get()
|
|
|
|
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
|
mesh = ifc_importer.create_camera(element, shape)
|
|
if element.is_a("IfcAnnotation") and ifc_importer.is_curve_annotation(element):
|
|
mesh = ifc_importer.create_curve(element, shape)
|
|
elif shape:
|
|
mesh = ifc_importer.create_mesh(element, shape)
|
|
ifc_importer.material_creator.load_existing_materials()
|
|
ifc_importer.material_creator.create(element, obj, mesh)
|
|
mesh.BIMMeshProperties.has_openings_applied = apply_openings
|
|
|
|
return mesh
|
|
|
|
@classmethod
|
|
def import_representation_parameters(cls, data):
|
|
props = data.BIMMeshProperties
|
|
elements = tool.Ifc.get().traverse(tool.Ifc.get().by_id(props.ifc_definition_id))
|
|
props.ifc_parameters.clear()
|
|
for element in elements:
|
|
if element.is_a("IfcRepresentationItem") or element.is_a("IfcParameterizedProfileDef"):
|
|
for i in range(0, len(element)):
|
|
if element.attribute_type(i) == "DOUBLE":
|
|
new = props.ifc_parameters.add()
|
|
new.name = "{}/{}".format(element.is_a(), element.attribute_name(i))
|
|
new.step_id = element.id()
|
|
new.type = element.attribute_type(i)
|
|
new.index = i
|
|
if element[i]:
|
|
new.value = element[i]
|
|
|
|
@classmethod
|
|
def is_body_representation(cls, representation: ifcopenshell.entity_instance) -> bool:
|
|
return representation.ContextOfItems.ContextIdentifier == "Body"
|
|
|
|
@classmethod
|
|
def is_box_representation(cls, representation: ifcopenshell.entity_instance) -> bool:
|
|
return representation.ContextOfItems.ContextIdentifier == "Box"
|
|
|
|
@classmethod
|
|
def is_edited(cls, obj):
|
|
return not all([tool.Cad.is_x(o, 1.0) for o in obj.scale]) or obj in IfcStore.edited_objs
|
|
|
|
@classmethod
|
|
def is_mapped_representation(cls, representation: ifcopenshell.entity_instance) -> bool:
|
|
return representation.RepresentationType == "MappedRepresentation"
|
|
|
|
@classmethod
|
|
def is_meshlike(cls, representation: ifcopenshell.entity_instance) -> bool:
|
|
if ifcopenshell.util.representation.resolve_representation(representation).RepresentationType in (
|
|
"AdvancedBrep",
|
|
"Annotation2D",
|
|
"Annotation3D",
|
|
"BoundingBox",
|
|
"Brep",
|
|
"Curve",
|
|
"Curve2D",
|
|
"Curve3D",
|
|
"FillArea",
|
|
"GeometricCurveSet",
|
|
"GeometricSet",
|
|
"Point",
|
|
"PointCloud",
|
|
"Surface",
|
|
"Surface2D",
|
|
"Surface3D",
|
|
"SurfaceModel",
|
|
"Tessellation",
|
|
):
|
|
return True
|
|
return False
|
|
|
|
@classmethod
|
|
def is_profile_based(cls, data):
|
|
return data.BIMMeshProperties.subshape_type == "PROFILE"
|
|
|
|
@classmethod
|
|
def is_swept_profile(cls, representation):
|
|
return ifcopenshell.util.representation.resolve_representation(representation).RepresentationType in (
|
|
"SweptSolid",
|
|
)
|
|
|
|
@classmethod
|
|
def is_text_literal(cls, representation):
|
|
items = ifcopenshell.util.representation.resolve_items(representation)
|
|
return bool([i for i in items if i["item"].is_a("IfcTextLiteral")])
|
|
|
|
@classmethod
|
|
def is_type_product(cls, element):
|
|
return element.is_a("IfcTypeProduct")
|
|
|
|
@classmethod
|
|
def link(cls, element, obj):
|
|
obj.BIMMeshProperties.ifc_definition_id = element.id()
|
|
|
|
@classmethod
|
|
def record_object_materials(cls, obj):
|
|
obj.data.BIMMeshProperties.material_checksum = str([s.id() for s in cls.get_styles(obj) if s])
|
|
|
|
@classmethod
|
|
def record_object_position(cls, obj: bpy.types.Object) -> None:
|
|
# These are recorded separately because they have different numerical tolerances
|
|
obj.BIMObjectProperties.location_checksum = repr(np.array(obj.matrix_world.translation).tobytes())
|
|
obj.BIMObjectProperties.rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes())
|
|
|
|
@classmethod
|
|
def remove_connection(cls, connection):
|
|
tool.Ifc.get().remove(connection)
|
|
|
|
@classmethod
|
|
def rename_object(cls, obj, name):
|
|
obj.name = name
|
|
|
|
@classmethod
|
|
def replace_object_with_empty(cls, obj: bpy.types.Object) -> None:
|
|
"""Recreate a Blender object as an empty object.
|
|
|
|
This method is useful when an object should no longer have associated
|
|
data (in Blender, you cannot simply assign .data to None). Note that the
|
|
object's original data is not handled by this method and should be
|
|
processed separately to avoid leaving orphan data.
|
|
"""
|
|
element = tool.Ifc.get_entity(obj)
|
|
name = obj.name
|
|
if element:
|
|
tool.Ifc.unlink(element=element)
|
|
|
|
obj.name = ifcopenshell.guid.new()
|
|
new_obj = bpy.data.objects.new(name, None)
|
|
|
|
if element:
|
|
tool.Ifc.link(element, new_obj)
|
|
for collection in obj.users_collection:
|
|
collection.objects.link(new_obj)
|
|
new_obj.matrix_world = obj.matrix_world
|
|
bpy.data.objects.remove(obj)
|
|
|
|
@classmethod
|
|
def resolve_mapped_representation(
|
|
cls, representation: ifcopenshell.entity_instance
|
|
) -> ifcopenshell.entity_instance:
|
|
if representation.RepresentationType == "MappedRepresentation":
|
|
return cls.resolve_mapped_representation(representation.Items[0].MappingSource.MappedRepresentation)
|
|
return representation
|
|
|
|
@classmethod
|
|
def unresolve_type_representation(cls, representation, occurence):
|
|
if not ifcopenshell.util.element.get_type(occurence):
|
|
return representation
|
|
|
|
if representation.RepresentationType == "MappedRepresentation":
|
|
return representation
|
|
|
|
for mapped_representation in occurence.Representation.Representations:
|
|
if cls.resolve_mapped_representation(mapped_representation) == representation:
|
|
return mapped_representation
|
|
|
|
@classmethod
|
|
def run_geometry_update_representation(cls, obj=None):
|
|
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="")
|
|
|
|
@classmethod
|
|
def run_style_add_style(cls, obj=None):
|
|
return blenderbim.core.style.add_style(tool.Ifc, tool.Style, obj=obj)
|
|
|
|
@classmethod
|
|
def select_connection(cls, connection):
|
|
obj = tool.Ifc.get_object(connection.RelatingElement)
|
|
if obj:
|
|
obj.select_set(True)
|
|
obj = tool.Ifc.get_object(connection.RelatedElement)
|
|
if obj:
|
|
obj.select_set(True)
|
|
|
|
@classmethod
|
|
def should_force_faceted_brep(cls):
|
|
return bpy.context.scene.BIMGeometryProperties.should_force_faceted_brep
|
|
|
|
@classmethod
|
|
def should_force_triangulation(cls):
|
|
return bpy.context.scene.BIMGeometryProperties.should_force_triangulation
|
|
|
|
@classmethod
|
|
def should_generate_uvs(cls, obj):
|
|
if tool.Ifc.get().schema == "IFC2X3":
|
|
return False
|
|
for slot in obj.material_slots:
|
|
if slot.material and slot.material.use_nodes:
|
|
for node in slot.material.node_tree.nodes:
|
|
if node.type == "TEX_COORD" and node.outputs["UV"].links:
|
|
return True
|
|
elif node.type == "UVMAP" and node.outputs["UV"].links and node.uv_map:
|
|
return True
|
|
return False
|
|
|
|
@classmethod
|
|
def should_use_presentation_style_assignment(cls) -> bool:
|
|
return bpy.context.scene.BIMGeometryProperties.should_use_presentation_style_assignment
|
|
|
|
@classmethod
|
|
def get_model_representations(cls) -> list[ifcopenshell.entity_instance]:
|
|
return tool.Ifc.get().by_type("IfcShapeRepresentation")
|
|
|
|
@classmethod
|
|
def flip_object(cls, obj, flip_local_axes):
|
|
assert len(flip_local_axes) == 2, "flip_local_axes must be two axes to flip"
|
|
rotation_axis = next(i for i in "XYZ" if i not in flip_local_axes)
|
|
rotation_axis_i = "XYZ".index(rotation_axis)
|
|
|
|
bb_data = tool.Blender.get_object_bounding_box(obj)
|
|
# min max points of rotated plane of origin based bounding box
|
|
min_point = Vector([min(i, 0) for i in bb_data["min_point"]])
|
|
max_point = Vector([max(i, 0) for i in bb_data["max_point"]])
|
|
# keep it in rotated plane only
|
|
max_point[rotation_axis_i] = min_point[rotation_axis_i]
|
|
|
|
# to compensate for flipped two axes
|
|
# we adjust new max point to match previous min point (or vice versa)
|
|
original_min_point = obj.matrix_world @ min_point
|
|
obj.matrix_world = obj.matrix_world @ Matrix.Rotation(pi, 4, rotation_axis)
|
|
new_max_point = obj.matrix_world @ max_point
|
|
obj.matrix_world.translation += original_min_point - new_max_point
|
|
|
|
bpy.context.view_layer.update()
|
|
|
|
@classmethod
|
|
def reload_representation(cls, obj_or_objs: Union[bpy.types.Object, Iterable[bpy.types.Object]]) -> None:
|
|
"""Reload object/objects active representation.
|
|
|
|
Ensures that same representations won't be reloaded multiple times.
|
|
"""
|
|
objs = obj_or_objs if isinstance(obj_or_objs, Iterable) else [obj_or_objs]
|
|
ifc_file = tool.Ifc.get()
|
|
|
|
# Find all objects that use the same representation
|
|
# as there are possibility that some of them have openings
|
|
# (each representation with opening has a unique Mesh)
|
|
# and therefore reloading Mesh of it's type or occurrence
|
|
# might not be enough.
|
|
elements = set()
|
|
for obj in objs:
|
|
representation = tool.Geometry.get_active_representation(obj)
|
|
if not representation:
|
|
continue
|
|
representation = tool.Geometry.resolve_mapped_representation(representation)
|
|
elements.update(ifcopenshell.util.element.get_elements_by_representation(ifc_file, representation))
|
|
|
|
# Filter out unique meshes to avoid
|
|
# reloading the same representation multiple times.
|
|
meshes_to_objects: dict[bpy.types.Mesh, bpy.types.Object]
|
|
meshes_to_objects = {(obj := tool.Ifc.get_object(element)).data: obj for element in elements}
|
|
|
|
for obj in meshes_to_objects.values():
|
|
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
|
blenderbim.core.geometry.switch_representation(
|
|
tool.Ifc,
|
|
tool.Geometry,
|
|
obj=obj,
|
|
representation=representation,
|
|
should_reload=True,
|
|
is_global=True,
|
|
should_sync_changes_first=False,
|
|
apply_openings=True,
|
|
)
|
|
|
|
@classmethod
|
|
def remove_representation_item(cls, representation_item):
|
|
# NOTE: we assume it's not the last representation item
|
|
# otherwise we probably would need to remove representation too
|
|
# NOTE: a lot of shared code with `geometry.remove_representation`
|
|
ifc_file = tool.Ifc.get()
|
|
shape_aspects = []
|
|
|
|
consider_inverses = []
|
|
styled_item, colour, texture, layer = None, None, None, None
|
|
[consider_inverses.append(styled_item := t) for t in representation_item.StyledByItem]
|
|
# IFC2X3 is using LayerAssignments
|
|
for t in (
|
|
representation_item.LayerAssignment
|
|
if hasattr(representation_item, "LayerAssignment")
|
|
else representation_item.LayerAssignments
|
|
):
|
|
consider_inverses.append(layer := t)
|
|
# IfcTessellatedFaceSet
|
|
[consider_inverses.append(colour := t) for t in getattr(representation_item, "HasColours", [])]
|
|
[consider_inverses.append(texture := t) for t in getattr(representation_item, "HasTextures", [])]
|
|
|
|
for inverse in ifc_file.get_inverse(representation_item):
|
|
if inverse.is_a("IfcShapeRepresentation"):
|
|
if inverse.OfShapeAspect:
|
|
shape_aspects.append(inverse.OfShapeAspect[0])
|
|
else:
|
|
representation = inverse
|
|
|
|
if styled_item:
|
|
ifc_file.remove(styled_item)
|
|
if layer and len(layer.Items) == 1:
|
|
ifc_file.remove(styled_item)
|
|
if colour:
|
|
ifcopenshell.util.element.remove_deep2(ifc_file, colour)
|
|
if texture:
|
|
ifcopenshell.util.element.remove_deep2(ifc_file, texture)
|
|
|
|
for shape_aspect in shape_aspects:
|
|
cls.remove_representation_items_from_shape_aspect([representation_item], shape_aspect)
|
|
|
|
representation.Items = tuple(set(representation.Items) - {representation_item})
|
|
also_consider = list(consider_inverses)
|
|
ifcopenshell.util.element.remove_deep2(ifc_file, representation_item, also_consider=also_consider)
|
|
|
|
@classmethod
|
|
def create_shape_aspect(cls, product_shape, base_representation, items, previous_shape_aspect=None):
|
|
"""
|
|
> `product_shape` - IfcProductDefinitionShape or IfcRepresentationMap\n
|
|
> `base_representation` - base representation to get context attributes from\n
|
|
> `items` - representation items\n
|
|
> `previous_shape_aspect` - (optional) previous shape aspect, if provided\n
|
|
items will be removed the previous shape aspect first\n
|
|
|
|
< IfcShapeAspect
|
|
"""
|
|
|
|
if previous_shape_aspect is not None:
|
|
cls.remove_representation_items_from_shape_aspect(items, previous_shape_aspect)
|
|
|
|
shape_aspect = tool.Ifc.get().createIfcShapeAspect(
|
|
PartOfProductDefinitionShape=product_shape, ShapeRepresentations=()
|
|
)
|
|
# keep IfcShapeAspect and IfcShapeRepresentation valid
|
|
rep = tool.Geometry.add_shape_aspect_representation(shape_aspect, base_representation)
|
|
rep.Items = items
|
|
|
|
return shape_aspect
|
|
|
|
@classmethod
|
|
def remove_representation_items_from_shape_aspect(cls, representation_items, shape_aspect):
|
|
ifc_file = tool.Ifc.get()
|
|
# as shape aspect might have multiple representations
|
|
# it's easier to find it from the item
|
|
for inverse in ifc_file.get_inverse(representation_items[0]):
|
|
if inverse.is_a("IfcShapeRepresentation") and shape_aspect in inverse.OfShapeAspect:
|
|
representation = inverse
|
|
break
|
|
|
|
# removing last item would make representation invalid
|
|
if len(representation.Items) == len(representation_items):
|
|
# removing last representation would make shape aspect invalid.
|
|
# remove shape aspect first otherwise remove_representation won't remove it because of the inverse
|
|
if len(shape_aspect.ShapeRepresentations) == 1:
|
|
ifc_file.remove(shape_aspect)
|
|
tool.Ifc.run("geometry.remove_representation", representation=representation)
|
|
else:
|
|
items = set(representation.Items) - set(representation_items)
|
|
representation.Items = tuple(items)
|
|
|
|
@classmethod
|
|
def add_representation_item_to_shape_aspect(cls, representation_items, shape_aspect):
|
|
"""NOTE: we assume that all items belonged to the same representation and to the same shape aspect"""
|
|
ifc_file = tool.Ifc.get()
|
|
previous_shape_aspect = None
|
|
for inverse in ifc_file.get_inverse(representation_items[0]):
|
|
if inverse.is_a("IfcShapeRepresentation"):
|
|
if inverse.OfShapeAspect:
|
|
# item is already added to the shape aspect
|
|
if inverse.OfShapeAspect[0] == shape_aspect:
|
|
return
|
|
previous_shape_aspect = inverse.OfShapeAspect[0]
|
|
else:
|
|
base_representation = inverse
|
|
|
|
# remove item from previous shape aspect
|
|
if previous_shape_aspect:
|
|
cls.remove_representation_items_from_shape_aspect(representation_items, previous_shape_aspect)
|
|
shape_aspect_representation = cls.get_shape_aspect_representation(
|
|
shape_aspect, base_representation, create_new=True
|
|
)
|
|
shape_aspect_representation.Items = shape_aspect_representation.Items + tuple(representation_items)
|
|
|
|
@classmethod
|
|
def get_shape_aspect_representation(cls, shape_aspect, base_representation, create_new=False):
|
|
for representation in shape_aspect.ShapeRepresentations:
|
|
if (
|
|
representation.ContextOfItems == base_representation.ContextOfItems
|
|
and representation.RepresentationIdentifier == base_representation.RepresentationIdentifier
|
|
and representation.RepresentationType == base_representation.RepresentationType
|
|
):
|
|
return representation
|
|
|
|
if not create_new:
|
|
return None
|
|
|
|
return cls.add_shape_aspect_representation(shape_aspect, base_representation)
|
|
|
|
@classmethod
|
|
def add_shape_aspect_representation(cls, shape_aspect, base_representation):
|
|
shape_aspect_representation = tool.Ifc.get().createIfcShapeRepresentation(
|
|
ContextOfItems=base_representation.ContextOfItems,
|
|
RepresentationIdentifier=base_representation.RepresentationIdentifier,
|
|
RepresentationType=base_representation.RepresentationType,
|
|
)
|
|
shape_aspect.ShapeRepresentations = shape_aspect.ShapeRepresentations + (shape_aspect_representation,)
|
|
return shape_aspect_representation
|
|
|
|
@classmethod
|
|
def get_shape_aspect_representation_for_item(cls, shape_aspect, representation_item):
|
|
ifc_file = tool.Ifc.get()
|
|
for inverse in ifc_file.get_inverse(representation_item):
|
|
if inverse.is_a("IfcShapeRepresentation"):
|
|
if inverse.OfShapeAspect:
|
|
if inverse.OfShapeAspect[0] == shape_aspect:
|
|
return inverse
|
|
|
|
@classmethod
|
|
def get_shape_aspect_styles(cls, element, shape_aspect, representation_item) -> list[ifcopenshell.entity_instance]:
|
|
"""update `representation_item` style based on styles connected to the `shape_aspect`
|
|
through material constituents with the same name
|
|
"""
|
|
if not shape_aspect.Name:
|
|
return []
|
|
|
|
# get material connected to the shape aspect with material constituent name
|
|
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
|
if not material or not material.is_a("IfcMaterialConstituentSet") or not material.MaterialConstituents:
|
|
return []
|
|
|
|
matching_constituent = next((c for c in material.MaterialConstituents if c.Name == shape_aspect.Name), None)
|
|
if matching_constituent is None:
|
|
return []
|
|
|
|
constituent_material = matching_constituent.Material
|
|
if not constituent_material.HasRepresentation:
|
|
return []
|
|
|
|
# get shape aspect representation for item
|
|
shape_aspect_representation = cls.get_shape_aspect_representation_for_item(shape_aspect, representation_item)
|
|
|
|
# get the styles for this context
|
|
material_representation = None
|
|
for r in constituent_material.HasRepresentation[0].Representations:
|
|
if r.ContextOfItems == shape_aspect_representation.ContextOfItems:
|
|
material_representation = r
|
|
break
|
|
|
|
if material_representation is None:
|
|
return []
|
|
|
|
styles = [s for s in tool.Ifc.get().traverse(material_representation) if s.is_a("IfcPresentationStyle")]
|
|
return styles
|
|
|
|
@classmethod
|
|
def delete_opening_object_placement(cls, placement):
|
|
model = tool.Ifc.get()
|
|
ifcopenshell.util.element.remove_deep2(model, placement)
|
|
|
|
@classmethod
|
|
def get_blender_offset_type(cls, obj: bpy.types.Object) -> Optional[str]:
|
|
props = bpy.context.scene.BIMGeoreferenceProperties
|
|
if props.has_blender_offset:
|
|
if (result := obj.BIMObjectProperties.blender_offset_type) == "NONE":
|
|
result = obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT"
|
|
return result
|