mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 15:53:00 +00:00
504 lines
21 KiB
Python
504 lines
21 KiB
Python
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
|
# Copyright (C) 2022 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 ifcopenshell
|
|
import ifcopenshell.util.unit
|
|
import ifcopenshell.util.placement
|
|
import ifcopenshell.util.representation
|
|
import blenderbim.core.tool
|
|
import blenderbim.tool as tool
|
|
import blenderbim.core.geometry as geometry
|
|
from mathutils import Matrix, Vector
|
|
from blenderbim.bim import import_ifc
|
|
from blenderbim.bim.module.geometry.helper import Helper
|
|
|
|
|
|
class Model(blenderbim.core.tool.Model):
|
|
@classmethod
|
|
def convert_si_to_unit(cls, value):
|
|
if isinstance(value, (tuple, list)):
|
|
return [v / cls.unit_scale for v in value]
|
|
return value / cls.unit_scale
|
|
|
|
@classmethod
|
|
def convert_unit_to_si(cls, value):
|
|
if isinstance(value, (tuple, list)):
|
|
return [v * cls.unit_scale for v in value]
|
|
return value * cls.unit_scale
|
|
|
|
@classmethod
|
|
def export_curve(cls, position, edge_indices, points=None):
|
|
position_i = position.inverted()
|
|
if len(edge_indices) == 2:
|
|
diameter = edge_indices[0]
|
|
p1 = cls.bm.verts[diameter[0]].co
|
|
p2 = cls.bm.verts[diameter[1]].co
|
|
center = cls.convert_si_to_unit(list(position_i @ p1.lerp(p2, 0.5)))
|
|
radius = cls.convert_si_to_unit((p1 - p2).length / 2)
|
|
return tool.Ifc.get().createIfcCircle(
|
|
tool.Ifc.get().createIfcAxis2Placement2D(tool.Ifc.get().createIfcCartesianPoint(center[0:2])), radius
|
|
)
|
|
if tool.Ifc.get().schema == "IFC2X3":
|
|
points = []
|
|
for edge in edge_indices:
|
|
local_point = (position_i @ Vector(cls.bm.verts[edge[0]].co)).to_2d()
|
|
points.append(tool.Ifc.get().createIfcCartesianPoint(cls.convert_si_to_unit(local_point)))
|
|
points.append(points[0])
|
|
return tool.Ifc.get().createIfcPolyline(points)
|
|
segments = []
|
|
for segment in edge_indices:
|
|
if len(segment) == 2:
|
|
segments.append(tool.Ifc.get().createIfcLineIndex([i + 1 for i in segment]))
|
|
elif len(segment) == 3:
|
|
segments.append(tool.Ifc.get().createIfcArcIndex([i + 1 for i in segment]))
|
|
return tool.Ifc.get().createIfcIndexedPolyCurve(cls.points, segments, False)
|
|
|
|
@classmethod
|
|
def export_points(cls, position, indices):
|
|
position_i = position.inverted()
|
|
points = []
|
|
for point in indices:
|
|
local_point = (position_i @ point).to_2d()
|
|
points.append(cls.convert_si_to_unit(list(local_point)))
|
|
return tool.Ifc.get().createIfcCartesianPointList2D(points)
|
|
|
|
@classmethod
|
|
def export_profile(cls, obj, position=None):
|
|
if position is None:
|
|
position = Matrix()
|
|
|
|
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
|
|
|
helper = Helper(tool.Ifc.get())
|
|
indices = helper.auto_detect_arbitrary_profile_with_voids(obj, obj.data)
|
|
|
|
if isinstance(indices, tuple) and indices[0] is False: # Ugly
|
|
return
|
|
|
|
cls.bm = bmesh.new()
|
|
cls.bm.from_mesh(obj.data)
|
|
cls.bm.verts.ensure_lookup_table()
|
|
cls.bm.edges.ensure_lookup_table()
|
|
|
|
if indices["inner_curves"]:
|
|
profile = tool.Ifc.get().createIfcArbitraryProfileDefWithVoids("AREA")
|
|
else:
|
|
profile = tool.Ifc.get().createIfcArbitraryClosedProfileDef("AREA")
|
|
|
|
if tool.Ifc.get().schema != "IFC2X3":
|
|
cls.points = cls.export_points(position, indices["points"])
|
|
|
|
profile.OuterCurve = cls.export_curve(position, indices["profile"])
|
|
if indices["inner_curves"]:
|
|
results = []
|
|
for inner_curve in indices["inner_curves"]:
|
|
results.append(cls.export_curve(position, inner_curve))
|
|
profile.InnerCurves = results
|
|
|
|
cls.bm.free()
|
|
return profile
|
|
|
|
@classmethod
|
|
def generate_occurrence_name(cls, element_type, ifc_class):
|
|
props = bpy.context.scene.BIMModelProperties
|
|
if props.occurrence_name_style == "CLASS":
|
|
return ifc_class[3:]
|
|
elif props.occurrence_name_style == "TYPE":
|
|
return element_type.Name or "Unnamed"
|
|
elif props.occurrence_name_style == "CUSTOM":
|
|
try:
|
|
# Power users gonna power
|
|
return eval(props.occurrence_name_function) or "Instance"
|
|
except:
|
|
return "Instance"
|
|
|
|
@classmethod
|
|
def get_extrusion(cls, representation):
|
|
item = representation.Items[0]
|
|
while True:
|
|
if item.is_a("IfcExtrudedAreaSolid"):
|
|
return item
|
|
elif item.is_a("IfcBooleanResult"):
|
|
item = item.FirstOperand
|
|
else:
|
|
break
|
|
|
|
@classmethod
|
|
def import_axis(cls, axis, obj=None, position=None):
|
|
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
|
|
|
if position is None:
|
|
position = Matrix()
|
|
|
|
cls.vertices = []
|
|
cls.edges = []
|
|
cls.arcs = []
|
|
cls.circles = []
|
|
|
|
if isinstance(axis, list):
|
|
cls.vertices.extend(
|
|
[
|
|
position @ Vector(cls.convert_unit_to_si(axis[0])).to_3d(),
|
|
position @ Vector(cls.convert_unit_to_si(axis[1])).to_3d(),
|
|
]
|
|
)
|
|
cls.edges.append([0, 1])
|
|
else:
|
|
cls.import_curve(obj, position, axis)
|
|
|
|
mesh = bpy.data.meshes.new("Axis")
|
|
mesh.from_pydata(cls.vertices, cls.edges, [])
|
|
mesh.BIMMeshProperties.subshape_type = "AXIS"
|
|
|
|
if obj is None:
|
|
obj = bpy.data.objects.new("Axis", mesh)
|
|
else:
|
|
obj.data = mesh
|
|
|
|
return obj
|
|
|
|
@classmethod
|
|
def import_profile(cls, profile, obj=None, position=None):
|
|
"""Creates new profile mesh and assigns it to `obj`,
|
|
if `obj` is `None` then new "Profile" object will be created.
|
|
|
|
Need to make sure to remove temporary mesh/object after use to avoid orphan data.
|
|
"""
|
|
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
|
|
|
if position is None:
|
|
position = Matrix()
|
|
|
|
cls.vertices = []
|
|
cls.edges = []
|
|
cls.arcs = []
|
|
cls.circles = []
|
|
|
|
if profile.is_a("IfcArbitraryClosedProfileDef"):
|
|
cls.import_curve(obj, position, profile.OuterCurve)
|
|
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
|
|
for inner_curve in profile.InnerCurves:
|
|
cls.import_curve(obj, position, inner_curve)
|
|
elif profile.is_a() == "IfcRectangleProfileDef":
|
|
cls.import_rectangle(obj, position, profile)
|
|
|
|
mesh = bpy.data.meshes.new("Profile")
|
|
mesh.from_pydata(cls.vertices, cls.edges, [])
|
|
mesh.BIMMeshProperties.subshape_type = "PROFILE"
|
|
|
|
if obj is None:
|
|
obj = bpy.data.objects.new("Profile", mesh)
|
|
else:
|
|
obj.data = mesh
|
|
|
|
for arc in cls.arcs:
|
|
group = obj.vertex_groups.new(name="IFCARCINDEX")
|
|
group.add(arc, 1, "REPLACE")
|
|
|
|
for circle in cls.circles:
|
|
group = obj.vertex_groups.new(name="IFCCIRCLE")
|
|
group.add(circle, 1, "REPLACE")
|
|
|
|
return obj
|
|
|
|
@classmethod
|
|
def import_curve(cls, obj, position, curve):
|
|
offset = len(cls.vertices)
|
|
|
|
if curve.is_a("IfcPolyline"):
|
|
total_points = len(curve.Points)
|
|
last_index = len(curve.Points) - 1
|
|
for i, point in enumerate(curve.Points):
|
|
if i == last_index:
|
|
continue
|
|
global_point = position @ Vector(cls.convert_unit_to_si(point.Coordinates)).to_3d()
|
|
cls.vertices.append(global_point)
|
|
cls.edges.extend([(i, i + 1) for i in range(offset, len(cls.vertices))])
|
|
cls.edges[-1] = (len(cls.vertices) - 1, offset) # Close the loop
|
|
elif curve.is_a("IfcIndexedPolyCurve"):
|
|
is_arc = False
|
|
is_closed = False
|
|
if curve.Segments:
|
|
for segment in curve.Segments:
|
|
if len(segment[0]) == 3: # IfcArcIndex
|
|
is_arc = True
|
|
local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1])
|
|
global_point = position @ Vector(local_point).to_3d()
|
|
cls.vertices.append(global_point)
|
|
local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment[0][1] - 1])
|
|
global_point = position @ Vector(local_point).to_3d()
|
|
cls.vertices.append(global_point)
|
|
cls.arcs.append([len(cls.vertices) - 2, len(cls.vertices) - 1])
|
|
else:
|
|
local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1])
|
|
global_point = position @ Vector(local_point).to_3d()
|
|
cls.vertices.append(global_point)
|
|
if is_arc:
|
|
cls.arcs[-1].append(len(cls.vertices) - 1)
|
|
is_arc = False
|
|
|
|
if curve.Segments[0][0][0] == curve.Segments[-1][0][-1]:
|
|
is_closed = True
|
|
else:
|
|
for local_point in curve.Points.CoordList:
|
|
global_point = position @ Vector(cls.convert_unit_to_si(local_point)).to_3d()
|
|
cls.vertices.append(global_point)
|
|
|
|
if cls.vertices[offset] == cls.vertices[-1]:
|
|
is_closed = True
|
|
del cls.vertices[-1]
|
|
|
|
cls.edges.extend([(i, i + 1) for i in range(offset, len(cls.vertices) - 1)])
|
|
if is_closed:
|
|
cls.edges.append([len(cls.vertices) - 1, offset]) # Close the loop
|
|
elif curve.is_a("IfcCircle"):
|
|
center = cls.convert_unit_to_si(
|
|
Matrix(ifcopenshell.util.placement.get_axis2placement(curve.Position).tolist()).col[3].to_3d()
|
|
)
|
|
radius = cls.convert_unit_to_si(curve.Radius)
|
|
cls.vertices.extend(
|
|
[
|
|
position @ Vector((center[0], center[1] - radius, 0.0)),
|
|
position @ Vector((center[0], center[1] + radius, 0.0)),
|
|
]
|
|
)
|
|
cls.circles.append([offset, offset + 1])
|
|
cls.edges.append((offset, offset + 1))
|
|
|
|
@classmethod
|
|
def import_rectangle(cls, obj, position, profile):
|
|
if profile.Position:
|
|
p_position = Matrix(ifcopenshell.util.placement.get_axis2placement(profile.Position).tolist())
|
|
p_position[0][3] *= cls.unit_scale
|
|
p_position[1][3] *= cls.unit_scale
|
|
p_position[2][3] *= cls.unit_scale
|
|
else:
|
|
p_position = Matrix()
|
|
|
|
x = cls.convert_unit_to_si(profile.XDim)
|
|
y = cls.convert_unit_to_si(profile.YDim)
|
|
|
|
cls.vertices.extend(
|
|
[
|
|
position @ p_position @ Vector((-x / 2, -y / 2, 0.0)),
|
|
position @ p_position @ Vector((x / 2, -y / 2, 0.0)),
|
|
position @ p_position @ Vector((x / 2, y / 2, 0.0)),
|
|
position @ p_position @ Vector((-x / 2, y / 2, 0.0)),
|
|
]
|
|
)
|
|
cls.edges.extend([(i, i + 1) for i in range(0, len(cls.vertices))])
|
|
cls.edges[-1] = (len(cls.vertices) - 1, 0) # Close the loop
|
|
|
|
@classmethod
|
|
def load_openings(cls, element, openings):
|
|
if not openings:
|
|
return []
|
|
obj = tool.Ifc.get_object(element)
|
|
ifc_import_settings = import_ifc.IfcImportSettings.factory()
|
|
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
|
|
ifc_importer.file = tool.Ifc.get()
|
|
ifc_importer.calculate_unit_scale()
|
|
ifc_importer.process_context_filter()
|
|
ifc_importer.material_creator.load_existing_materials()
|
|
openings = set(openings)
|
|
openings -= ifc_importer.create_products(openings)
|
|
for opening in openings or []:
|
|
if tool.Ifc.get_object(opening):
|
|
continue
|
|
opening_obj = ifc_importer.create_product(opening)
|
|
if obj:
|
|
opening_obj.parent = obj
|
|
opening_obj.matrix_parent_inverse = obj.matrix_world.inverted()
|
|
for obj in ifc_importer.added_data.values():
|
|
bpy.context.scene.collection.objects.link(obj)
|
|
return ifc_importer.added_data.values()
|
|
|
|
@classmethod
|
|
def clear_scene_openings(cls):
|
|
props = bpy.context.scene.BIMModelProperties
|
|
has_deleted_opening = True
|
|
while has_deleted_opening:
|
|
has_deleted_opening = False
|
|
for i, opening in enumerate(props.openings):
|
|
if not opening.obj:
|
|
props.openings.remove(i)
|
|
has_deleted_opening = True
|
|
|
|
@classmethod
|
|
def get_material_layer_parameters(cls, element):
|
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
|
offset = 0.0
|
|
thickness = 0.0
|
|
direction_sense = "POSITIVE"
|
|
material = ifcopenshell.util.element.get_material(element)
|
|
if material:
|
|
if material.is_a("IfcMaterialLayerSetUsage"):
|
|
offset = material.OffsetFromReferenceLine * unit_scale
|
|
direction_sense = material.DirectionSense
|
|
material = material.ForLayerSet
|
|
if material.is_a("IfcMaterialLayerSet"):
|
|
thickness = sum([l.LayerThickness for l in material.MaterialLayers]) * unit_scale
|
|
if direction_sense == "NEGATIVE":
|
|
thickness *= -1
|
|
offset *= -1
|
|
return {"thickness": thickness, "offset": offset, "direction_sense": direction_sense}
|
|
|
|
@classmethod
|
|
def get_manual_booleans(cls, element):
|
|
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
|
if not body:
|
|
return []
|
|
booleans = []
|
|
items = list(body.Items)
|
|
while items:
|
|
item = items.pop()
|
|
if item.is_a() == "IfcBooleanResult":
|
|
booleans.append(item)
|
|
items.append(item.FirstOperand)
|
|
elif item.is_a("IfcBooleanClippingResult"):
|
|
items.append(item.FirstOperand)
|
|
return booleans
|
|
|
|
@classmethod
|
|
def get_usage_type(cls, element):
|
|
material = ifcopenshell.util.element.get_material(element, should_inherit=False)
|
|
if material:
|
|
if material.is_a("IfcMaterialLayerSetUsage"):
|
|
return f"LAYER{material.LayerSetDirection[-1]}"
|
|
elif material.is_a("IfcMaterialProfileSetUsage"):
|
|
return "PROFILE"
|
|
|
|
@classmethod
|
|
def get_wall_axis(cls, obj, layers=None):
|
|
x_values = [v[0] for v in obj.bound_box]
|
|
min_x = min(x_values)
|
|
max_x = max(x_values)
|
|
axes = {}
|
|
if layers:
|
|
axes = {
|
|
"base": [
|
|
(obj.matrix_world @ Vector((min_x, layers["offset"], 0.0))).to_2d(),
|
|
(obj.matrix_world @ Vector((max_x, layers["offset"], 0.0))).to_2d(),
|
|
],
|
|
"side": [
|
|
(obj.matrix_world @ Vector((min_x, layers["offset"] + layers["thickness"], 0.0))).to_2d(),
|
|
(obj.matrix_world @ Vector((max_x, layers["offset"] + layers["thickness"], 0.0))).to_2d(),
|
|
],
|
|
}
|
|
axes["reference"] = [
|
|
(obj.matrix_world @ Vector((min_x, 0.0, 0.0))).to_2d(),
|
|
(obj.matrix_world @ Vector((max_x, 0.0, 0.0))).to_2d(),
|
|
]
|
|
return axes
|
|
|
|
@classmethod
|
|
def regenerate_array(cls, parent, data):
|
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
|
obj_stack = [parent]
|
|
|
|
for array in data:
|
|
child_i = 0
|
|
existing_children = set(array["children"])
|
|
total_existing_children = len(array["children"])
|
|
children_elements = []
|
|
children_objs = []
|
|
if array['dimension_input_type'] == "Total":
|
|
divider = 1 if ((array["count"] - 1) == 0) else (array["count"] - 1)
|
|
base_offset = Vector([array["x"] / divider, array["y"] / divider, array["z"] / divider]) * unit_scale
|
|
else:
|
|
base_offset = Vector([array["x"], array["y"], array["z"]]) * unit_scale
|
|
for i in range(array["count"]):
|
|
if i == 0:
|
|
continue
|
|
offset = base_offset * i
|
|
for obj in obj_stack:
|
|
if child_i >= total_existing_children:
|
|
child_obj = tool.Spatial.duplicate_object_and_data(obj)
|
|
child_element = tool.Spatial.run_root_copy_class(obj=child_obj)
|
|
else:
|
|
global_id = array["children"][child_i]
|
|
try:
|
|
child_element = tool.Ifc.get().by_guid(global_id)
|
|
child_obj = tool.Ifc.get_object(child_element)
|
|
assert child_obj
|
|
except:
|
|
child_obj = tool.Spatial.duplicate_object_and_data(obj)
|
|
child_element = tool.Spatial.run_root_copy_class(obj=child_obj)
|
|
|
|
child_psets = ifcopenshell.util.element.get_psets(child_element)
|
|
child_pset = child_psets.get("BBIM_Array")
|
|
if child_pset:
|
|
ifcopenshell.api.run(
|
|
"pset.edit_pset",
|
|
tool.Ifc.get(),
|
|
pset=tool.Ifc.get().by_id(child_pset["id"]),
|
|
properties={"Data": None},
|
|
)
|
|
|
|
new_matrix = obj.matrix_world.copy()
|
|
if array["use_local_space"]:
|
|
current_obj_translation = obj.matrix_world @ offset
|
|
else:
|
|
current_obj_translation = obj.matrix_world.col[3].to_3d() + offset
|
|
new_matrix.col[3] = current_obj_translation.to_4d()
|
|
child_obj.matrix_world = new_matrix
|
|
children_objs.append(child_obj)
|
|
children_elements.append(child_element)
|
|
child_i += 1
|
|
obj_stack.extend(children_objs)
|
|
array["children"] = [e.GlobalId for e in children_elements]
|
|
|
|
removed_children = set(existing_children) - set(array["children"])
|
|
for removed_child in removed_children:
|
|
element = tool.Ifc.get().by_guid(removed_child)
|
|
obj = tool.Ifc.get_object(element)
|
|
if obj:
|
|
bpy.data.objects.remove(obj)
|
|
# TODO: Not sufficient, refactor OverrideDeleteTrait
|
|
|
|
bpy.context.view_layer.update()
|
|
|
|
@classmethod
|
|
def replace_object_ifc_representation(cls, ifc_context, obj, new_representation):
|
|
ifc_file = tool.Ifc.get()
|
|
ifc_element = tool.Ifc.get_entity(obj)
|
|
old_representation = ifcopenshell.util.representation.get_representation(
|
|
ifc_element, ifc_context.ContextType, ifc_context.ContextIdentifier, ifc_context.TargetView
|
|
)
|
|
|
|
if old_representation:
|
|
old_representation = tool.Geometry.resolve_mapped_representation(old_representation)
|
|
for inverse in ifc_file.get_inverse(old_representation):
|
|
ifcopenshell.util.element.replace_attribute(inverse, old_representation, new_representation)
|
|
ifcopenshell.api.run("geometry.remove_representation", ifc_file, representation=old_representation)
|
|
else:
|
|
ifcopenshell.api.run(
|
|
"geometry.assign_representation", ifc_file, product=ifc_element, representation=new_representation
|
|
)
|
|
geometry.switch_representation(
|
|
tool.Ifc,
|
|
tool.Geometry,
|
|
obj=obj,
|
|
representation=new_representation,
|
|
should_reload=True,
|
|
is_global=True,
|
|
should_sync_changes_first=False,
|
|
)
|