Merge branch 'v0.8.0' of https://github.com/IfcOpenShell/IfcOpenShell into update_type_manager

This commit is contained in:
Gorgious56
2025-01-13 09:38:41 +01:00
113 changed files with 3990 additions and 732 deletions
Binary file not shown.
+1 -1
View File
@@ -134,7 +134,7 @@ def update_bim_tool_props():
if AuthoringData.data["active_material_usage"] == "LAYER2":
x_angle = get_x_angle(extrusion)
axis = tool.Model.get_wall_axis(obj)["reference"]
props.extrusion_depth = extrusion.Depth * si_conversion * cos(x_angle)
props.extrusion_depth = abs(extrusion.Depth * si_conversion * cos(x_angle))
props.length = (axis[1] - axis[0]).length
props.x_angle = x_angle
-17
View File
@@ -261,8 +261,6 @@ class IfcImporter:
self.profile_code("Setup arrays")
tool.Project.load_linked_models_from_ifc()
self.profile_code("Load linked models")
self.lock_scales()
self.profile_code("Lock objects scales")
self.add_project_to_scene()
self.profile_code("Add project to scene")
if self.ifc_import_settings.should_clean_mesh and len(self.file.by_type("IfcElement")) < 1000:
@@ -1130,21 +1128,6 @@ class IfcImporter:
bpy.context.scene.BIMAggregateProperties.aggregate_decorator = True
def lock_scales(self) -> None:
elements = set(self.file.by_type("IfcProduct"))
while elements:
element = elements.pop()
if not getattr(element, "HasOpenings", False):
continue
voided_elements = tool.Aggregate.get_parts_recursively(element)
voided_elements.add(element)
elements.difference_update(voided_elements)
for element in voided_elements:
if not (obj := tool.Ifc.get_object(element)):
continue
tool.Geometry.lock_scale(obj)
class IfcImportSettings:
def __init__(self):
self.logger: logging.Logger = None
@@ -207,7 +207,6 @@ class AggregateDecorator:
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
parts_objs = [tool.Ifc.get_object(p) for p in parts]
indices, edges = create_bounding_box(parts_objs)
self.line_shader.uniform_float("lineWidth", 0.5)
self.draw_batch("LINES", indices, color, edges)
@@ -230,9 +229,7 @@ class AggregateModeDecorator:
cls.handlers.append(
SpaceView3D.draw_handler_add(handler.draw_aggregate_empty, (context,), "WINDOW", "POST_VIEW")
)
cls.handlers.append(
SpaceView3D.draw_handler_add(handler.draw_new_objects, (context,), "WINDOW", "POST_VIEW")
)
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_new_objects, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@classmethod
@@ -352,12 +349,11 @@ class AggregateModeDecorator:
new_objs = [o for o in new_objs if o.data]
for obj in new_objs:
element = tool.Ifc.get_entity(obj)
if (aggregate := ifcopenshell.util.element.get_aggregate(element)) == tool.Ifc.get_entity(props.editing_aggregate):
if (aggregate := ifcopenshell.util.element.get_aggregate(element)) == tool.Ifc.get_entity(
props.editing_aggregate
):
continue
if element and element.is_a("IfcElement"):
data = ItemDecorator.get_obj_data(obj)
if data:
self.draw_batch(
"TRIS", data["verts"], transparent_color((1, 0, 0, 1)), data["tris"]
)
self.draw_batch("TRIS", data["verts"], transparent_color((1, 0, 0, 1)), data["tris"])
@@ -375,6 +375,7 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
return {"FINISHED"}
class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
bl_idname = "bim.disable_aggregate_mode"
bl_label = "Disable Aggregate Mode"
@@ -385,6 +386,7 @@ class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
return {"FINISHED"}
class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
bl_idname = "bim.toggle_aggregate_mode_local_view"
bl_label = "Toggle Aggregate Mode Local View"
@@ -403,6 +405,7 @@ class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
return {"FINISHED"}
class BIM_OT_aggregate_assign_new_objects_in_aggregate_mode(bpy.types.Operator):
bl_idname = "bim.aggregate_assign_new_objects_in_aggregate_mode"
bl_label = "Aggregate Assign New Objects In Aggregate Mode"
@@ -424,7 +427,9 @@ class BIM_OT_aggregate_assign_new_objects_in_aggregate_mode(bpy.types.Operator):
new_objs = [o for o in new_objs if o.data]
for obj in new_objs:
element = tool.Ifc.get_entity(obj)
if (aggregate := ifcopenshell.util.element.get_aggregate(element)) == tool.Ifc.get_entity(props.editing_aggregate):
if (aggregate := ifcopenshell.util.element.get_aggregate(element)) == tool.Ifc.get_entity(
props.editing_aggregate
):
continue
if element and element.is_a("IfcElement"):
obj.select_set(True)
@@ -439,4 +444,3 @@ class BIM_OT_aggregate_assign_new_objects_in_aggregate_mode(bpy.types.Operator):
BIM_OT_aggregate_assign_object._execute(self, context)
return {"FINISHED"}
@@ -313,10 +313,8 @@ class BIM_PT_material_classifications(Panel, ReferenceUI):
if not tool.Ifc.get():
return False
props = context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
return True
if props.is_editing and (material := props.active_material) and material.ifc_definition_id:
return True
return False
def draw(self, context):
@@ -18,6 +18,7 @@
import bpy
from . import ui, prop, operator
from bpy.app.handlers import persistent
classes = (
operator.AddCurvelikeItem,
@@ -89,7 +90,20 @@ classes = (
addon_keymaps = []
@persistent
def block_scale(scene):
if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active):
if isinstance(obj, bpy.types.Object) and obj.BIMObjectProperties.ifc_definition_id:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
elif isinstance(obj, bpy.types.Mesh) and obj.BIMMeshProperties.ifc_definition_id:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
def register():
bpy.app.handlers.depsgraph_update_pre.append(block_scale)
operator.OverrideDuplicateMoveMacro.define("BIM_OT_override_object_duplicate_move")
operator.OverrideDuplicateMoveMacro.define("TRANSFORM_OT_translate")
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
@@ -158,6 +172,8 @@ def register():
def unregister():
bpy.app.handlers.depsgraph_update_pre.remove(block_scale)
bpy.types.VIEW3D_MT_object.remove(ui.object_menu)
bpy.types.OUTLINER_MT_object.remove(ui.outliner_menu)
bpy.types.VIEW3D_MT_object_context_menu.remove(ui.outliner_menu)
@@ -454,7 +454,7 @@ class Helper:
x_axis = (mesh.vertices[loop[0]].co - center).normalized()
else:
x_axis = (mesh.vertices[loop[1]].co - mesh.vertices[loop[0]].co).normalized()
z_axis = profile_face.polygons[0].normal.normalized() * (-1)
z_axis = profile_face.polygons[0].normal.normalized() * -1
y_axis = z_axis.cross(x_axis).normalized()
matrix = Matrix((x_axis, y_axis, z_axis))
matrix.normalize()
@@ -1858,7 +1858,7 @@ class OverrideEscape(bpy.types.Operator):
bpy.ops.bim.hide_all_openings()
elif context.scene.BIMAggregateProperties.in_aggregate_mode:
bpy.ops.bim.disable_aggregate_mode()
elif active_object:=context.active_object:
elif active_object := context.active_object:
if tool.Blender.Modifier.try_canceling_editing_modifier_parameters_or_path(active_object):
pass
return {"FINISHED"}
@@ -1902,7 +1902,9 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly")
elif obj in [o.obj for o in context.scene.BIMAggregateProperties.not_editing_objects]:
obj.select_set(False)
self.report({"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly")
self.report(
{"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly"
)
elif obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs:
self.report({"ERROR"}, "Clipping planes cannot be edited")
elif element:
@@ -1971,6 +1973,13 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
elif item.is_a("IfcAnnotationFillArea"):
tool.Model.import_annotation_fill_area(item, obj=obj)
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
self.enable_edit_mode(context)
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
elif tool.Geometry.is_curvelike_item(item):
tool.Model.import_curve(item, obj=obj)
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
@@ -2124,10 +2133,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry.import_item(obj)
elif item.is_a("IfcSweptAreaSolid"):
ProfileDecorator.uninstall()
profile = tool.Model.export_profile(obj)
if not profile:
if not (profile := tool.Model.export_profile(obj)):
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
@@ -2184,6 +2190,26 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
product=element,
representation=new_footprint,
)
elif item.is_a("IfcAnnotationFillArea"):
ProfileDecorator.uninstall()
if not (profile := tool.Model.export_annotation_fill_area(obj)):
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
ProfileDecorator.install(bpy.context)
self.enable_edit_mode(bpy.context)
return
for inverse in tool.Ifc.get().get_inverse(item):
ifcopenshell.util.element.replace_attribute(inverse, item, profile)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item)
obj.data.BIMMeshProperties.ifc_definition_id = profile.id()
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
elif tool.Geometry.is_curvelike_item(item):
ProfileDecorator.uninstall()
new = tool.Model.export_curves(obj)
@@ -2924,7 +2950,7 @@ class OverrideMoveAggregate(bpy.types.Operator):
obj.select_set(False)
continue
element = tool.Ifc.get_entity(obj)
if not element or props.in_aggregate_mode:
if not element or not element.is_a("IfcElement") or props.in_aggregate_mode:
continue
parts = ifcopenshell.util.element.get_parts(element)
if parts:
@@ -124,6 +124,11 @@ class BIMMaterialProperties(PropertyGroup):
styles: EnumProperty(items=get_styles, name="Styles")
contexts: EnumProperty(items=get_contexts, name="Contexts")
@property
def active_material(self):
if self.active_material_index < len(self.materials):
return self.materials[self.active_material_index]
class BIMObjectMaterialProperties(PropertyGroup):
material_type: EnumProperty(items=get_object_material_type, name="Material Type")
@@ -95,6 +95,7 @@ classes = (
profile.ChangeCardinalPoint,
profile.ChangeProfileDepth,
profile.DisableEditingExtrusionAxis,
profile.DrawPolylineProfile,
profile.EditExtrusionAxis,
profile.EnableEditingExtrusionAxis,
profile.ExtendProfile,
@@ -127,6 +128,7 @@ classes = (
prop.SnapMousePoint,
prop.PolylinePoint,
prop.Polyline,
prop.ProductPreviewItem,
prop.BIMModelProperties,
prop.BIMArrayProperties,
prop.BIMStairProperties,
@@ -136,6 +138,7 @@ classes = (
prop.BIMRailingProperties,
prop.BIMRoofProperties,
prop.BIMPolylineProperties,
prop.BIMProductPreviewProperties,
ui.BIM_PT_array,
ui.BIM_PT_stair,
ui.BIM_PT_sverchok,
@@ -218,6 +221,7 @@ def register():
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
bpy.types.Scene.BIMPolylineProperties = bpy.props.PointerProperty(type=prop.BIMPolylineProperties)
bpy.types.Scene.BIMProductPreviewProperties = bpy.props.PointerProperty(type=prop.BIMProductPreviewProperties)
bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties)
bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties)
bpy.types.Object.BIMSverchokProperties = bpy.props.PointerProperty(type=prop.BIMSverchokProperties)
@@ -248,6 +252,7 @@ def unregister():
del bpy.types.Scene.BIMModelProperties
del bpy.types.Scene.BIMPolylineProperties
del bpy.types.Scene.BIMProductPreviewProperties
del bpy.types.Object.BIMArrayProperties
del bpy.types.Object.BIMStairProperties
del bpy.types.Object.BIMSverchokProperties
+12 -330
View File
@@ -25,15 +25,14 @@ import bmesh
import ifcopenshell
import bonsai.tool as tool
import math
from math import sin, cos, tan, radians
from math import sin, cos, radians
from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
from mathutils import Vector, Matrix, Quaternion
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
from gpu_extras.presets import draw_circle_2d
from typing import Union
from bonsai.bim.module.drawing.helper import format_distance
from bonsai.bim.module.geometry.decorator import ItemDecorator
def transparent_color(color, alpha=0.1):
@@ -786,6 +785,10 @@ class ProductDecorator:
@classmethod
def uninstall(cls):
props = bpy.context.scene.BIMProductPreviewProperties # updated by model/polyline.py
props.verts.clear()
props.edges.clear()
props.tris.clear()
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
@@ -799,315 +802,12 @@ class ProductDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def get_wall_preview_data(cls, context, relating_type):
def create_bmesh_from_vertices(vertices):
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in polyline_vertices]
if is_closed:
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
new_edges.append(
bm.edges.new((new_verts[-1], new_verts[0]))
) # Add an edge between the last an first point to make it closed.
else:
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.verts.index_update()
bm.edges.index_update()
return bm
# Get properties from object type
layers = tool.Model.get_material_layer_parameters(relating_type)
if not layers["thickness"]:
return
thickness = layers["thickness"]
model_props = context.scene.BIMModelProperties
direction_sense = model_props.direction_sense
direction = 1
if direction_sense == "NEGATIVE":
direction = -1
offset_type = model_props.offset_type
offset = 0
if offset_type == "CENTER":
offset = -thickness / 2
elif offset_type == "INTERIOR":
offset = -thickness
unit_system = tool.Drawing.get_unit_system()
factor = 1
if unit_system == "IMPERIAL":
factor = 3.048
if unit_system == "METRIC":
unit_length = context.scene.unit_settings.length_unit
if unit_length == "MILLIMETERS":
factor = 1000
# For the model properties, the offset value should just be converted
# However, for the wall preview logic that follows, offset and thickness must change direction
model_props.offset = offset * factor
thickness *= direction
offset *= direction
height = float(model_props.extrusion_depth)
rl = float(model_props.rl1)
x_angle = float(model_props.x_angle)
angle_distortion = height * tan(x_angle)
wall_preview_data = {}
wall_preview_data["verts"] = []
# Verts
polyline_vertices = []
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) < 2:
wall_preview_data = []
return
for point in polyline_points:
polyline_vertices.append(Vector((point.x, point.y, point.z)))
is_closed = False
if (
polyline_vertices[0].x == polyline_vertices[-1].x
and polyline_vertices[0].y == polyline_vertices[-1].y
and polyline_vertices[0].z == polyline_vertices[-1].z
):
is_closed = True
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
bm_base = create_bmesh_from_vertices(polyline_vertices)
base_vertices = tool.Cad.offset_edges(bm_base, offset)
offset_base_verts = tool.Cad.offset_edges(bm_base, thickness + offset)
top_vertices = tool.Cad.offset_edges(bm_base, angle_distortion + offset)
offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distortion + thickness + offset)
if is_closed:
base_vertices.append(base_vertices[0])
offset_base_verts.append(offset_base_verts[0])
top_vertices.append(top_vertices[0])
offset_top_verts.append(offset_top_verts[0])
if offset_base_verts is not None:
for v in base_vertices:
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
for v in offset_base_verts[::-1]:
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
for v in top_vertices:
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
for v in offset_top_verts[::-1]:
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
bm_base.free()
# Edges and Tris
points = []
side_edges_1 = []
side_edges_2 = []
base_edges = []
for i in range(len(wall_preview_data["verts"])):
points.append(Vector(wall_preview_data["verts"][i]))
n = len(points) // 2
bottom_side_1 = [[i, (i + 1) % (n)] for i in range((n - 1) // 2)]
bottom_side_2 = [[i, (i + 1) % (n)] for i in range(n // 2, n - 1)]
bottom_connections = [[i, n - i - 1] for i in range(n // 2)]
bottom_loop = bottom_connections + bottom_side_1 + bottom_side_2
side_edges_1.extend(bottom_side_1)
side_edges_2.extend(bottom_side_2)
base_edges.extend(bottom_loop)
upper_side_1 = [[i + n for i in edges] for edges in bottom_side_1]
upper_side_2 = [[i + n for i in edges] for edges in bottom_side_2]
upper_loop = [[i + n for i in edges] for edges in bottom_loop]
side_edges_1.extend(upper_side_1)
side_edges_2.extend(upper_side_2)
base_edges.extend(upper_loop)
loops = [side_edges_1, side_edges_2, base_edges]
wall_preview_data["edges"] = []
wall_preview_data["tris"] = []
for i, group in enumerate(loops):
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in points]
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in group]
bm.verts.index_update()
bm.edges.index_update()
if i == 2:
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
new_faces = bmesh.ops.bridge_loops(bm, edges=bm.edges, use_pairs=True, use_cyclic=True)
bm.verts.index_update()
bm.edges.index_update()
edges = [[v.index for v in e.verts] for e in bm.edges]
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
wall_preview_data["edges"].extend(edges)
wall_preview_data["tris"].extend(tris)
wall_preview_data["edges"] = list(set(tuple(e) for e in wall_preview_data["edges"]))
wall_preview_data["tris"] = list(set(tuple(t) for t in wall_preview_data["tris"]))
return wall_preview_data
def get_product_preview_data(cls, context, relating_type):
model_props = context.scene.BIMModelProperties
if relating_type.is_a("IfcDoorType"):
rl = float(model_props.rl1)
elif relating_type.is_a("IfcWindowType"):
rl = float(model_props.rl2)
else:
rl = 0
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
snap_obj = bpy.data.objects.get(snap_prop.snap_object)
snap_element = tool.Ifc.get_entity(snap_obj)
rot_mat = Quaternion()
if snap_element and snap_element.is_a("IfcWall"):
rot_mat = snap_obj.matrix_world.to_quaternion()
obj_type = tool.Ifc.get_object(relating_type)
if obj_type.data:
data = ItemDecorator.get_obj_data(obj_type)
data["verts"] = [tuple(obj_type.matrix_world.inverted() @ Vector(v)) for v in data["verts"]]
data["verts"] = [tuple(rot_mat @ (Vector((v[0], v[1], (v[2] + rl)))) + mouse_point) for v in data["verts"]]
return data
def get_profile_preview_data(self, context, relating_type):
material = ifcopenshell.util.element.get_material(relating_type)
try:
profile = material.MaterialProfiles[0].Profile
except:
return {}
model_props = context.scene.BIMModelProperties
extrusion_depth = model_props.extrusion_depth
cardinal_point = model_props.cardinal_point
rot_mat = Quaternion()
if relating_type.is_a("IfcBeamType"):
y_rot = Quaternion((0.0, 1.0, 0.0), radians(90))
z_rot = Quaternion((0.0, 0.0, 1.0), radians(90))
rot_mat = y_rot @ z_rot
# Get profile data
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, profile)
verts = shape.verts
if not verts:
raise RuntimeError("Profile shape has no vertices, it probably is invalid.")
edges = shape.edges
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
# Create offsets based on cardinal point
min_x = min(v[0] for v in grouped_verts)
max_x = max(v[0] for v in grouped_verts)
min_y = min(v[1] for v in grouped_verts)
max_y = max(v[1] for v in grouped_verts)
x_offset = (max_x - min_x) / 2
y_offset = (max_y - min_y) / 2
match cardinal_point:
case "1":
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "2":
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
case "3":
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "4":
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
case "5":
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
case "6":
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
case "7":
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
case "8":
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
case "9":
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
# Create extrusion bmesh
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in grouped_verts]
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(grouped_verts) - 1)]
bm.verts.index_update()
bm.edges.index_update()
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, extrusion_depth))
bm.verts.index_update()
bm.edges.index_update()
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
# Create bounding box
verts = bm.verts
i = len(verts)
min_x = min(v.co.x for v in verts)
max_x = max(v.co.x for v in verts)
min_y = min(v.co.y for v in verts)
max_y = max(v.co.y for v in verts)
min_z = min(v.co.z for v in verts)
max_z = max(v.co.z for v in verts)
bbox_verts = [
(min_x, min_y, min_z),
(max_x, min_y, min_z),
(max_x, max_y, min_z),
(min_x, max_y, min_z),
(min_x, min_y, max_z),
(max_x, min_y, max_z),
(max_x, max_y, max_z),
(min_x, max_y, max_z),
]
bbox_edges = [
(0 + i, 3 + i),
(3 + i, 7 + i),
(7 + i, 4 + i),
(4 + i, 0 + i),
(0 + i, 1 + i),
(3 + i, 2 + i),
(7 + i, 6 + i),
(4 + i, 5 + i),
(1 + i, 2 + i),
(2 + i, 6 + i),
(6 + i, 5 + i),
(5 + i, 1 + i),
]
# Calculate rotation, mouse position, angle and cardinal point
# TODO Angle
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
def get_product_preview_data(self, context):
props = context.scene.BIMProductPreviewProperties
data = {}
verts = [tuple(v.co) for v in verts]
verts.extend(bbox_verts)
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
verts = [tuple(Vector(v) + mouse_point) for v in verts]
data["verts"] = verts
data["edges"] = bbox_edges
# data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges]
data["tris"] = tris
bm.free()
data["verts"] = [(*v.value_3d,) for v in props.verts]
data["edges"] = [(int(e.value_2d[0]), int(e.value_2d[1])) for e in props.edges]
data["tris"] = [(int(t.value_3d[0]), int(t.value_3d[1]), int(t.value_3d[2])) for t in props.tris]
return data
def draw_product_preview(self, context):
@@ -1134,25 +834,7 @@ class ProductDecorator:
else:
return
# Wall
if self.relating_type.is_a("IfcWallType"):
wall_preview_data = self.get_wall_preview_data(context, self.relating_type)
if wall_preview_data:
self.draw_batch("LINES", wall_preview_data["verts"], decorator_color, wall_preview_data["edges"])
self.draw_batch(
"TRIS", wall_preview_data["verts"], transparent_color(decorator_color), wall_preview_data["tris"]
)
# Mesh type products
product_preview_data = self.get_product_preview_data(context, self.relating_type)
if product_preview_data:
self.draw_batch("LINES", product_preview_data["verts"], decorator_color, product_preview_data["edges"])
self.draw_batch(
"TRIS", product_preview_data["verts"], transparent_color(decorator_color), product_preview_data["tris"]
)
# Profile type products
product_preview_data = self.get_profile_preview_data(context, self.relating_type)
product_preview_data = self.get_product_preview_data(context)
if product_preview_data:
self.draw_batch("LINES", product_preview_data["verts"], decorator_color, product_preview_data["edges"])
self.draw_batch(
+8 -26
View File
@@ -19,8 +19,6 @@
import bpy
import bmesh
from bmesh.types import BMVert, BMFace
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
@@ -169,22 +167,6 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None:
tool.Model.update_simple_openings(element)
# TODO: move it out to tools
def bm_sort_out_geom(
geom_data: list[Union[bmesh.types.BMVert, bmesh.types.BMEdge, bmesh.types.BMFace]]
) -> dict[str, Any]:
geom_dict = {"verts": [], "edges": [], "faces": []}
for el in geom_data:
if isinstance(el, BMVert):
geom_dict["verts"].append(el)
elif isinstance(el, BMFace):
geom_dict["faces"].append(el)
else:
geom_dict["edges"].append(el)
return geom_dict
def bm_mirror(
bm: bmesh.types.BMesh,
verts: list[bmesh.types.BMVert],
@@ -208,7 +190,7 @@ def bm_mirror(
for v in verts:
faces.update(v.link_faces)
duplicated = bmesh.ops.duplicate(bm, geom=list(faces))
verts = bm_sort_out_geom(duplicated["geom"])["verts"]
verts = tool.Model.bm_sort_out_geom(duplicated["geom"])["verts"]
bmesh.ops.transform(bm, verts=verts, matrix=matrix, space=Matrix.Identity(4))
return verts
@@ -242,7 +224,7 @@ def create_bm_extruded_profile(
extruded = bmesh.ops.extrude_face_region(bm, geom=new_faces)
extrusion_vector = extrusion_vector * magnitude
extruded_verts = bm_sort_out_geom(extruded["geom"])["verts"]
extruded_verts = tool.Model.bm_sort_out_geom(extruded["geom"])["verts"]
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
bmesh.ops.translate(bm, vec=position, verts=new_verts + extruded_verts)
@@ -304,7 +286,7 @@ def create_bm_door_lining(
extruded = bmesh.ops.extrude_face_region(bm, geom=new_faces)
extrusion_vector = Vector((0, 1, 0)) * depth
translate_verts = [v for v in extruded["geom"] if isinstance(v, BMVert)]
translate_verts = [v for v in extruded["geom"] if isinstance(v, bmesh.types.BMVert)]
bmesh.ops.translate(bm, vec=extrusion_vector, verts=translate_verts)
bmesh.ops.translate(bm, vec=position, verts=new_verts + translate_verts)
@@ -572,7 +554,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
update_door_modifier_representation(obj)
def _execute(self, context):
for obj in context.selected_objects:
for obj in tool.Blender.get_selected_objects():
if not tool.Blender.Modifier.is_eligible_for_door_modifier(obj):
continue
self.add_door_on_object(obj)
@@ -610,7 +592,7 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing = False
def _execute(self, context):
for obj in context.selected_objects:
for obj in tool.Blender.get_selected_objects():
self.cancel_editing_door_on_object(obj)
return {"FINISHED"}
@@ -642,7 +624,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": door_data})
def _execute(self, context):
for obj in context.selected_objects:
for obj in tool.Blender.get_selected_objects():
self.finish_editing_door_on_object(obj)
return {"FINISHED"}
@@ -666,7 +648,7 @@ class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing = True
def _execute(self, context):
for obj in context.selected_objects:
for obj in tool.Blender.get_selected_objects():
self.edit_door_on_obj(obj)
return {"FINISHED"}
@@ -686,6 +668,6 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
def _execute(self, context):
for obj in context.selected_objects:
for obj in tool.Blender.get_selected_objects():
self.remove_door_on_object(obj)
return {"FINISHED"}
+456 -2
View File
@@ -35,14 +35,440 @@ import bonsai.core.geometry
import bonsai.core.model as core
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from math import pi, sin, cos, degrees
from mathutils import Vector, Matrix
from math import pi, sin, cos, degrees, tan
from mathutils import Vector, Matrix, Quaternion
from bonsai.bim.module.model.opening import FilledOpeningGenerator
from bonsai.bim.module.model.decorator import PolylineDecorator
from bonsai.bim.module.geometry.decorator import ItemDecorator
from typing import Optional, Union, Literal
from lark import Lark, Transformer
def get_wall_preview_data(context, relating_type):
def create_bmesh_from_vertices(vertices):
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in polyline_vertices]
if is_closed:
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
new_edges.append(
bm.edges.new((new_verts[-1], new_verts[0]))
) # Add an edge between the last an first point to make it closed.
else:
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.verts.index_update()
bm.edges.index_update()
return bm
# Get properties from object type
layers = tool.Model.get_material_layer_parameters(relating_type)
if not layers["thickness"]:
return
thickness = layers["thickness"]
model_props = context.scene.BIMModelProperties
direction_sense = model_props.direction_sense
direction = 1
if direction_sense == "NEGATIVE":
direction = -1
offset_type = model_props.offset_type
offset = 0
if offset_type == "CENTER":
offset = -thickness / 2
elif offset_type == "INTERIOR":
offset = -thickness
unit_system = tool.Drawing.get_unit_system()
factor = 1
if unit_system == "IMPERIAL":
factor = 3.048
if unit_system == "METRIC":
unit_length = context.scene.unit_settings.length_unit
if unit_length == "MILLIMETERS":
factor = 1000
# For the model properties, the offset value should just be converted
# However, for the wall preview logic that follows, offset and thickness must change direction
model_props.offset = offset * factor
thickness *= direction
offset *= direction
height = float(model_props.extrusion_depth)
rl = float(model_props.rl1)
x_angle = float(model_props.x_angle)
angle_distortion = height * tan(x_angle)
data = {}
data["verts"] = []
# Verts
polyline_vertices = []
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) < 2:
data = []
return
for point in polyline_points:
polyline_vertices.append(Vector((point.x, point.y, point.z)))
is_closed = False
if (
polyline_vertices[0].x == polyline_vertices[-1].x
and polyline_vertices[0].y == polyline_vertices[-1].y
and polyline_vertices[0].z == polyline_vertices[-1].z
):
is_closed = True
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
bm_base = create_bmesh_from_vertices(polyline_vertices)
base_vertices = tool.Cad.offset_edges(bm_base, offset)
offset_base_verts = tool.Cad.offset_edges(bm_base, thickness + offset)
top_vertices = tool.Cad.offset_edges(bm_base, angle_distortion + offset)
offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distortion + thickness + offset)
if is_closed:
base_vertices.append(base_vertices[0])
offset_base_verts.append(offset_base_verts[0])
top_vertices.append(top_vertices[0])
offset_top_verts.append(offset_top_verts[0])
if offset_base_verts is not None:
for v in base_vertices:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
for v in offset_base_verts[::-1]:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
for v in top_vertices:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
for v in offset_top_verts[::-1]:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
bm_base.free()
# Edges and Tris
points = []
side_edges_1 = []
side_edges_2 = []
base_edges = []
for i in range(len(data["verts"])):
points.append(Vector(data["verts"][i]))
n = len(points) // 2
bottom_side_1 = [[i, (i + 1) % (n)] for i in range((n - 1) // 2)]
bottom_side_2 = [[i, (i + 1) % (n)] for i in range(n // 2, n - 1)]
bottom_connections = [[i, n - i - 1] for i in range(n // 2)]
bottom_loop = bottom_connections + bottom_side_1 + bottom_side_2
side_edges_1.extend(bottom_side_1)
side_edges_2.extend(bottom_side_2)
base_edges.extend(bottom_loop)
upper_side_1 = [[i + n for i in edges] for edges in bottom_side_1]
upper_side_2 = [[i + n for i in edges] for edges in bottom_side_2]
upper_loop = [[i + n for i in edges] for edges in bottom_loop]
side_edges_1.extend(upper_side_1)
side_edges_2.extend(upper_side_2)
base_edges.extend(upper_loop)
loops = [side_edges_1, side_edges_2, base_edges]
data["edges"] = []
data["tris"] = []
for i, group in enumerate(loops):
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in points]
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in group]
bm.verts.index_update()
bm.edges.index_update()
if i == 2:
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
new_faces = bmesh.ops.bridge_loops(bm, edges=bm.edges, use_pairs=True, use_cyclic=True)
bm.verts.index_update()
bm.edges.index_update()
edges = [[v.index for v in e.verts] for e in bm.edges]
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
data["edges"].extend(edges)
data["tris"].extend(tris)
data["edges"] = list(set(tuple(e) for e in data["edges"]))
data["tris"] = list(set(tuple(t) for t in data["tris"]))
return data
def get_vertical_profile_preview_data(context, relating_type):
material = ifcopenshell.util.element.get_material(relating_type)
try:
profile = material.MaterialProfiles[0].Profile
except:
return {}
model_props = context.scene.BIMModelProperties
extrusion_depth = model_props.extrusion_depth
cardinal_point = model_props.cardinal_point
rot_mat = Quaternion()
if relating_type.is_a("IfcBeamType"):
y_rot = Quaternion((0.0, 1.0, 0.0), radians(90))
z_rot = Quaternion((0.0, 0.0, 1.0), radians(90))
rot_mat = y_rot @ z_rot
# Get profile data
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, profile)
verts = shape.verts
if not verts:
raise RuntimeError("Profile shape has no vertices, it probably is invalid.")
edges = shape.edges
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
# Create offsets based on cardinal point
min_x = min(v[0] for v in grouped_verts)
max_x = max(v[0] for v in grouped_verts)
min_y = min(v[1] for v in grouped_verts)
max_y = max(v[1] for v in grouped_verts)
x_offset = (max_x - min_x) / 2
y_offset = (max_y - min_y) / 2
match cardinal_point:
case "1":
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "2":
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
case "3":
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "4":
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
case "5":
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
case "6":
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
case "7":
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
case "8":
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
case "9":
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
# Create extrusion bmesh
bm = bmesh.new()
grouped_verts.append(grouped_verts[0]) # Close profile
new_verts = [bm.verts.new(v) for v in grouped_verts]
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(grouped_verts) - 1)]
bm.verts.index_update()
bm.edges.index_update()
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces, use_dissolve_ortho_edges=True)
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, extrusion_depth))
bm.verts.index_update()
bm.edges.index_update()
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
# Calculate rotation, mouse position, angle and cardinal point
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
data = {}
verts = [tuple(v.co) for v in bm.verts]
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
verts = [tuple(Vector(v) + mouse_point) for v in verts]
min_z = min(v.co.z for v in bm.verts)
max_z = max(v.co.z for v in bm.verts)
# Add axis verts
verts.append(tuple(mouse_point))
verts.append(tuple(mouse_point + Vector((0, 0, max_z))))
# Add only profile edges
edges = []
for edge in bm.edges:
if (edge.verts[0].co.z == min_z and edge.verts[1].co.z == min_z) or (
edge.verts[0].co.z == max_z and edge.verts[1].co.z == max_z
):
edges.append(edge)
# Add axis edge
edges = [(edge.verts[0].index, edge.verts[1].index) for edge in edges]
edges.append((len(verts) - 1, len(verts) - 2))
data["verts"] = verts
data["edges"] = edges
data["tris"] = tris
bm.free()
return data
def get_horizontal_profile_preview_data(context, relating_type):
material = ifcopenshell.util.element.get_material(relating_type)
try:
profile_curve = material.MaterialProfiles[0].Profile
except:
return {}
model_props = context.scene.BIMModelProperties
cardinal_point = model_props.cardinal_point
polyline_verts = []
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) < 2:
return
for point in polyline_points:
polyline_verts.append(Vector((point.x, point.y, point.z)))
polyline_edges = [(i, i + 1) for i in range(len(polyline_verts) - 1)]
# Get profile shape
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, profile_curve)
verts = shape.verts
if not verts:
raise RuntimeError("Profile shape has no vertices, it probably is invalid.")
edges = shape.edges
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
# Create offsets based on cardinal point
min_x = min(v[0] for v in grouped_verts)
max_x = max(v[0] for v in grouped_verts)
min_y = min(v[1] for v in grouped_verts)
max_y = max(v[1] for v in grouped_verts)
x_offset = (max_x - min_x) / 2
y_offset = (max_y - min_y) / 2
match cardinal_point:
case "1":
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "2":
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
case "3":
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "4":
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
case "5":
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
case "6":
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
case "7":
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
case "8":
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
case "9":
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
# Create profile curve
scale_mat = Matrix.Scale(-1, 4, (1.0, 0.0, 0.0))
grouped_verts = [scale_mat @ Vector(v) for v in grouped_verts]
profile_curve = bpy.data.curves.new("Profile", type="CURVE")
profile_curve.dimensions = "2D"
profile_curve.splines.new("POLY")
profile_curve.splines[0].points.add(len(grouped_verts))
for i, point in enumerate(profile_curve.splines[0].points):
if i == len(grouped_verts): # Close curve
point.co = Vector((*grouped_verts[0], 0))
continue
point.co = Vector((*grouped_verts[i], 0))
profile_obj = bpy.data.objects.new("Profile", profile_curve)
# Create path curve with profile object as bevel
path_curve = bpy.data.curves.new("Polyline", type="CURVE")
path_curve.dimensions = "2D"
path_curve.splines.new("POLY")
path_curve.splines[0].points.add(len(polyline_verts) - 1)
for i, point in enumerate(path_curve.splines[0].points):
point.co = Vector((*polyline_verts[i], 0))
path_curve.splines[0].use_smooth = False
path_curve.bevel_mode = "OBJECT"
path_curve.bevel_object = profile_obj
# Convert path curve to mesh
# This operation throws a warning when done during gpu drawing, so it was removed from the decorator file to be handled here
path_obj = bpy.data.objects.new("Preview", path_curve)
context.scene.collection.objects.link(path_obj)
bpy.context.view_layer.objects.active = path_obj
dg = context.evaluated_depsgraph_get()
path_obj = path_obj.evaluated_get(dg)
me = path_obj.to_mesh()
# Create bmesh from path mesh
bm = bmesh.new()
new_verts = [bm.verts.new(v.co) for v in me.vertices]
index = [[v for v in edge.vertices] for edge in me.edges]
new_edges = [bm.edges.new((new_verts[i[0]], new_verts[i[1]])) for i in index]
for face in me.polygons:
verts = [new_verts[i] for i in face.vertices]
bm.faces.new(verts)
bm.verts.index_update()
bm.edges.index_update()
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
bpy.data.objects.remove(bpy.data.objects[path_obj.name], do_unlink=True)
bpy.data.objects.remove(bpy.data.objects[profile_obj.name], do_unlink=True)
try:
bpy.data.curves.remove(profile_obj.data, do_unlink=True)
except:
pass
try:
bpy.data.curves.remove(path_obj.data, do_unlink=True)
except:
pass
data = {}
data["verts"] = [tuple(v.co) for v in bm.verts]
data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges]
data["tris"] = tris
bm.free()
return data
def get_product_preview_data(context, relating_type):
model_props = context.scene.BIMModelProperties
if relating_type.is_a("IfcDoorType"):
rl = float(model_props.rl1)
elif relating_type.is_a("IfcWindowType"):
rl = float(model_props.rl2)
else:
rl = 0
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
mouse_point = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
snap_obj = bpy.data.objects.get(snap_prop.snap_object)
snap_element = tool.Ifc.get_entity(snap_obj)
rot_mat = Quaternion()
if snap_element and snap_element.is_a("IfcWall"):
rot_mat = snap_obj.matrix_world.to_quaternion()
obj_type = tool.Ifc.get_object(relating_type)
if obj_type.data:
data = ItemDecorator.get_obj_data(obj_type)
data["verts"] = [tuple(obj_type.matrix_world.inverted() @ Vector(v)) for v in data["verts"]]
data["verts"] = [tuple(rot_mat @ (Vector((v[0], v[1], (v[2] + rl)))) + mouse_point) for v in data["verts"]]
return data
class PolylineOperator:
# TODO Fill doc strings
""" """
@@ -387,6 +813,34 @@ class PolylineOperator:
tool.Polyline.remove_last_polyline_point()
tool.Blender.update_viewport()
def get_product_preview_data(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_isntance):
if tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type.is_a() not in {"IfcColumnType"}:
data = get_horizontal_profile_preview_data(context, relating_type)
elif tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type.is_a() in {"IfcColumnType"}:
data = get_vertical_profile_preview_data(context, relating_type)
elif tool.Model.get_usage_type(relating_type) == "LAYER2":
data = get_wall_preview_data(context, relating_type)
else:
data = get_product_preview_data(context, relating_type)
# Update properties so it can be used by the decorator
if not data:
return
props = context.scene.BIMProductPreviewProperties
props.verts.clear()
props.edges.clear()
props.tris.clear()
for vert in data["verts"]:
v = props.verts.add()
v.value_3d = vert
for edge in data["edges"]:
e = props.edges.add()
e.value_2d = edge
for tri in data["tris"]:
t = props.tris.add()
t.value_3d = tri
def modal(self, context: bpy.types.Context, event: bpy.types.Event) -> Union[set[str], None]:
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
tool.Blender.update_viewport()
@@ -191,6 +191,8 @@ class AddOccurrence(bpy.types.Operator, PolylineOperator):
if event.value == "RELEASE" and event.type == "LEFTMOUSE":
self.create_occurrence(context, event)
self.get_product_preview_data(context, self.relating_type)
cancel = self.handle_cancelation(context, event)
if cancel is not None:
ProductDecorator.uninstall()
+161 -5
View File
@@ -32,11 +32,13 @@ import bonsai.core.type
import bonsai.core.geometry
import bonsai.core.material
import bonsai.core.root
from math import pi, degrees, inf
from math import pi, degrees, inf, atan2
from mathutils import Vector, Matrix, Quaternion
from bonsai.bim.module.geometry.helper import Helper
from bonsai.bim.module.model.wall import DumbWallRecalculator
from bonsai.bim.module.model.decorator import ProfileDecorator
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
from typing import Union, Any
class DumbProfileGenerator:
@@ -44,7 +46,7 @@ class DumbProfileGenerator:
self.relating_type = relating_type
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
def generate(self):
def generate(self, insertion_type="CURSOR"):
self.file = tool.Ifc.get()
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
material = ifcopenshell.util.element.get_material(self.relating_type)
@@ -67,7 +69,28 @@ class DumbProfileGenerator:
self.rotation = 0
self.location = Vector((0, 0, 0))
self.cardinal_point = int(bpy.context.scene.BIMModelProperties.cardinal_point)
return self.derive_from_cursor()
if insertion_type == "POLYLINE":
return self.derive_from_polyline()
elif insertion_type == "CURSOR":
return self.derive_from_cursor()
def derive_from_polyline(self) -> tuple[list[Union[dict[str, Any], None]], bool]:
polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
is_polyline_closed = False
if len(polyline_points) > 3:
first_vec = Vector((polyline_points[0].x, polyline_points[0].y, polyline_points[0].z))
last_vec = Vector((polyline_points[-1].x, polyline_points[-1].y, polyline_points[-1].z))
if first_vec == last_vec:
is_polyline_closed = True
profiles = []
for i in range(len(polyline_points) - 1):
vec1 = Vector((polyline_points[i].x, polyline_points[i].y, polyline_points[i].z))
vec2 = Vector((polyline_points[i + 1].x, polyline_points[i + 1].y, polyline_points[i + 1].z))
coords = (vec1, vec2)
profiles.append(self.create_profile_from_2_points(coords))
return profiles, is_polyline_closed
def derive_from_cursor(self):
self.location = bpy.context.scene.cursor.location
@@ -82,10 +105,13 @@ class DumbProfileGenerator:
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
matrix_world = Matrix()
if self.relating_type.is_a() in ["IfcBeamType", "IfcMemberType"] or self.relating_type.is_a(
if self.relating_type.is_a() in ["IfcBeamType", "IfcCoveringType", "IfcMemberType"] or self.relating_type.is_a(
"IfcFlowSegmentType"
):
matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world
matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world
matrix_world.translation = self.location
if self.container_obj:
matrix_world.translation.z = self.container_obj.location.z
@@ -146,6 +172,25 @@ class DumbProfileGenerator:
return obj
def create_profile_from_2_points(self, coords, should_round=False) -> Union[dict[str, Any], None]:
direction = coords[1] - coords[0]
length = direction.length
if round(length, 4) < 0.1:
return
data = {"coords": coords}
self.depth = length
self.rotation = atan2(direction[1], direction[0])
if should_round:
# Round to nearest 50mm (yes, metric for now)
self.length = 0.05 * round(length / 0.05)
# Round to nearest 5 degrees
nearest_degree = (math.pi / 180) * 5
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
self.location = coords[0]
data["obj"] = self.create_profile()
return data
class DumbProfileRegenerator:
def regenerate_from_profile_def(self, profile):
@@ -1064,3 +1109,114 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
joiner = DumbProfileJoiner()
joiner.set_depth(obj, depth)
return {"FINISHED"}
class DrawPolylineProfile(bpy.types.Operator, PolylineOperator):
bl_idname = "bim.draw_polyline_profile"
bl_label = "Draw Polyline Profile"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.space_data.type == "VIEW_3D"
def __init__(self):
super().__init__()
self.relating_type = None
props = bpy.context.scene.BIMModelProperties
relating_type_id = props.relating_type_id
if relating_type_id:
self.relating_type = tool.Ifc.get().by_id(int(relating_type_id))
def create_profiles_from_polyline(self, context: bpy.types.Context) -> Union[set[str], None]:
if not self.relating_type:
return {"FINISHED"}
model_props = context.scene.BIMModelProperties
direction_sense = model_props.direction_sense
offset = model_props.offset
profiles, is_polyline_closed = DumbProfileGenerator(self.relating_type).generate("POLYLINE")
if profiles:
if is_polyline_closed:
for profile1, profile2 in zip(profiles, profiles[1:] + [profiles[0]]):
DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"])
else:
for profile1, profile2 in zip(profiles[:-1], profiles[1:]):
DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"])
def modal(self, context, event):
if not self.relating_type:
self.report({"WARNING"}, "You need to select a profile type.")
PolylineDecorator.uninstall()
tool.Blender.update_viewport()
return {"FINISHED"}
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
tool.Blender.update_viewport()
self.handle_lock_axis(context, event) # Must come before "PASS_TRHOUGH"
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
self.handle_mouse_move(context, event)
return {"PASS_THROUGH"}
# Wall axis settings
if event.value == "RELEASE" and event.type == "F":
direction_sense = context.scene.BIMModelProperties.direction_sense
context.scene.BIMModelProperties.direction_sense = (
"NEGATIVE" if direction_sense == "POSITIVE" else "POSITIVE"
)
if event.value == "RELEASE" and event.type == "O":
offset_type = context.scene.BIMModelProperties.offset_type
items = ["EXTERIOR", "CENTER", "INTERIOR"]
index = items.index(offset_type)
size = len(items)
context.scene.BIMModelProperties.offset_type = items[((index + 1) % size)]
props = bpy.context.scene.BIMModelProperties
wall_config = f"""Direction: {props.direction_sense}
Offset Type: {props.offset_type}
Offset Value: {props.offset}
"""
self.handle_instructions(context, wall_config)
self.handle_mouse_move(context, event, should_round=True)
self.choose_axis(event)
self.handle_snap_selection(context, event)
if (
not self.tool_state.is_input_on
and event.value == "RELEASE"
and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
):
self.create_profiles_from_polyline(context)
context.workspace.status_text_set(text=None)
ProductDecorator.uninstall()
PolylineDecorator.uninstall()
tool.Polyline.clear_polyline()
tool.Blender.update_viewport()
return {"FINISHED"}
self.handle_keyboard_input(context, event)
self.handle_inserting_polyline(context, event)
self.get_product_preview_data(context, self.relating_type)
cancel = self.handle_cancelation(context, event)
if cancel is not None:
ProductDecorator.uninstall()
return cancel
return {"RUNNING_MODAL"}
def invoke(self, context, event):
super().invoke(context, event)
ProductDecorator.install(context)
self.tool_state.use_default_container = True
self.tool_state.plane_method = "XY"
return {"RUNNING_MODAL"}
+21 -2
View File
@@ -112,6 +112,12 @@ def update_search_name(self, context):
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
def update_x_angle(self, context):
angle_deg = math.degrees(self.x_angle)
if tool.Cad.is_x(angle_deg, -90, 0.5) or tool.Cad.is_x(angle_deg, 90, 0.5):
self.x_angle = 0
class BIMModelProperties(PropertyGroup):
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
relating_type_id: bpy.props.EnumProperty(
@@ -183,8 +189,10 @@ class BIMModelProperties(PropertyGroup):
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
rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation")
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE", min=-pi / 180 * 89, max=pi / 180 * 89)
type_page: bpy.props.IntProperty(name="Type Page", default=1, min=1,update=update_type_page)
type_page: bpy.props.IntProperty(name="Type Page", default=1, min=1, update=update_type_page)
x_angle: bpy.props.FloatProperty(
name="X Angle", default=0, subtype="ANGLE", min=math.radians(-180), max=math.radians(180), update=update_x_angle
)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
direction_sense: bpy.props.EnumProperty(
@@ -842,3 +850,14 @@ class BIMPolylineProperties(PropertyGroup):
snap_mouse_ref: bpy.props.CollectionProperty(type=SnapMousePoint)
insertion_polyline: bpy.props.CollectionProperty(type=Polyline)
measurement_polyline: bpy.props.CollectionProperty(type=Polyline)
class ProductPreviewItem(PropertyGroup):
value_3d: bpy.props.FloatVectorProperty()
value_2d: bpy.props.FloatVectorProperty(size=2)
class BIMProductPreviewProperties(PropertyGroup):
verts: bpy.props.CollectionProperty(type=ProductPreviewItem)
edges: bpy.props.CollectionProperty(type=ProductPreviewItem)
tris: bpy.props.CollectionProperty(type=ProductPreviewItem)
@@ -26,7 +26,6 @@ import ifcopenshell.util.unit
import bonsai.core.root
import bonsai.core.geometry
import bonsai.tool as tool
from bonsai.bim.module.model.door import bm_sort_out_geom
from bonsai.bim.module.model.data import RailingData, refresh
from bonsai.bim.module.model.decorator import ProfileDecorator
@@ -47,7 +46,7 @@ def bm_split_edge_at_offset(edge: bmesh.types.BMEdge, offset: float) -> dict[str
split_output_0 = bmesh.utils.edge_split(edge, v0, offset / edge_len)
split_output_1 = bmesh.utils.edge_split(edge, v1, offset / (edge_len - offset))
new_geometry = bm_sort_out_geom(split_output_0 + split_output_1)
new_geometry = tool.Model.bm_sort_out_geom(split_output_0 + split_output_1)
return new_geometry
@@ -175,11 +174,11 @@ def update_railing_modifier_bmesh(context: bpy.types.Context) -> None:
ortho_vector = edge_dir.cross(V_(0, 0, 1))
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
extruded_verts = tool.Model.bm_sort_out_geom(extruded_geom)["verts"]
bmesh.ops.translate(bm, vec=ortho_vector * (-thickness / 2), verts=extruded_verts)
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
extruded_verts = tool.Model.bm_sort_out_geom(extruded_geom)["verts"]
bmesh.ops.translate(bm, vec=ortho_vector * (thickness / 2), verts=extruded_verts)
# dissolve middle edge
@@ -187,7 +186,7 @@ def update_railing_modifier_bmesh(context: bpy.types.Context) -> None:
# height
extruded_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
extruded_verts = tool.Model.bm_sort_out_geom(extruded_geom)["verts"]
extrusion_vector = Vector((0, 0, 1)) * height
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
+1 -2
View File
@@ -26,7 +26,6 @@ import ifcopenshell.util.unit
import bonsai.core.root
import bonsai.tool as tool
from bonsai.bim.helper import convert_property_group_from_si
from bonsai.bim.module.model.door import bm_sort_out_geom
from bonsai.bim.module.model.data import RoofData, refresh
from bonsai.bim.module.model.decorator import ProfileDecorator
@@ -327,7 +326,7 @@ def generate_hiped_roof_bmesh(
bmesh.ops.delete(bm, geom=bottom_chords_to_remove, context="EDGES")
# add roof thickness
extrusion_geom = bm_sort_out_geom(bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"])
extrusion_geom = tool.Model.bm_sort_out_geom(bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"])
extruded_edges = extrusion_geom["edges"]
extruded_verts = extrusion_geom["verts"]
rafter_edge_angle = pi / 2 - rafter_edge_angle
@@ -421,6 +421,8 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator):
self.handle_inserting_polyline(context, event)
self.get_product_preview_data(context, self.relating_type)
cancel = self.handle_cancelation(context, event)
if cancel is not None:
ProductDecorator.uninstall()
@@ -118,10 +118,6 @@ class BimTool(WorkSpaceTool):
EditObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
else:
CreateObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
# Show some UI for spatial elements that are unselectable by default.
if active_ifc_object:
EditObjectUI.layout = layout # Prevent .draw_modes from using old layout and crash.
EditObjectUI.draw_modes(context)
class WallTool(BimTool):
@@ -955,6 +951,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
def hotkey_S_A(self):
props = bpy.context.scene.BIMModelProperties
relating_type_id = AuthoringData.data["relating_type_id_current"]
relating_type_class = AuthoringData.data["ifc_class_current"]
if relating_type_id is None:
self.report({"ERROR"}, "No relating type selected")
return
@@ -994,6 +991,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
relating_type_id and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER3"
):
bpy.ops.bim.draw_polyline_slab("INVOKE_DEFAULT")
elif (
relating_type_id
and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "PROFILE"
and relating_type_class not in {"IfcColumnType"}
):
bpy.ops.bim.draw_polyline_profile("INVOKE_DEFAULT")
else:
bpy.ops.bim.add_occurrence("INVOKE_DEFAULT")
@@ -98,12 +98,6 @@ class ExecuteIfcPatch(bpy.types.Operator):
if props.should_load_from_memory and tool.Ifc.get():
args["file"] = tool.Ifc.get()
if ifcpatch.get_patch_input_argument_use(recipe_name) == "REQUIRED":
self.report(
{"ERROR"},
f"The recipe '{recipe_name}' is not currently supported if file is loaded from memory.",
)
return {"CANCELLED"}
else:
args["input"] = cast(str, props.ifc_patch_input)
args["file"] = cast(ifcopenshell.file, ifcopenshell.open(props.ifc_patch_input))
@@ -47,7 +47,6 @@ classes = (
ui.BIM_PT_object_psets,
ui.BIM_PT_object_qtos,
ui.BIM_PT_material_psets,
ui.BIM_PT_material_set_psets,
ui.BIM_PT_material_set_item_psets,
ui.BIM_PT_task_qtos,
ui.BIM_PT_resource_qtos,
@@ -66,7 +65,6 @@ classes = (
def register():
bpy.types.Object.PsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.MaterialPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Object.MaterialSetPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Object.MaterialSetItemPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.TaskPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.ResourcePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
@@ -82,7 +80,6 @@ def register():
def unregister():
del bpy.types.Object.PsetProperties
del bpy.types.Scene.MaterialPsetProperties
del bpy.types.Object.MaterialSetPsetProperties
del bpy.types.Object.MaterialSetItemPsetProperties
del bpy.types.Scene.TaskPsetProperties
del bpy.types.Scene.ResourcePsetProperties
+1 -18
View File
@@ -33,7 +33,6 @@ def refresh():
ObjectPsetsData.is_loaded = False
ObjectQtosData.is_loaded = False
MaterialPsetsData.is_loaded = False
MaterialSetPsetsData.is_loaded = False
MaterialSetItemPsetsData.is_loaded = False
TaskQtosData.is_loaded = False
ResourceQtosData.is_loaded = False
@@ -185,7 +184,7 @@ class MaterialPsetsData(Data):
if material.ifc_definition_id:
material = tool.Ifc.get().by_id(material.ifc_definition_id)
category = getattr(material, "Category", None) or None
psets = bonsai.bim.schema.ifc.psetqto.get_applicable("IfcMaterial", category, pset_only=True)
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(props.material_type, category, pset_only=True)
psetnames = cls.format_pset_enum(psets)
assigned_names = ifcopenshell.util.element.get_psets(
material, psets_only=True, should_inherit=False
@@ -194,22 +193,6 @@ class MaterialPsetsData(Data):
return []
class MaterialSetPsetsData(Data):
data = {}
is_loaded = False
@classmethod
def load(cls):
psets = {}
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
if material and "Set" in material.is_a():
psets = cls.psetqtos(material)
cls.data = {"psets": psets}
cls.is_loaded = True
class MaterialSetItemPsetsData(Data):
data = {}
is_loaded = False
+6 -47
View File
@@ -25,7 +25,6 @@ from bonsai.bim.module.pset.data import (
ObjectPsetsData,
ObjectQtosData,
MaterialPsetsData,
MaterialSetPsetsData,
MaterialSetItemPsetsData,
TaskQtosData,
ResourceQtosData,
@@ -88,7 +87,7 @@ def draw_enumerated_property(
def get_active_pset_obj_name(context: bpy.types.Context, obj_type: tool.Ifc.OBJECT_TYPE) -> str:
if obj_type in ("Object", "MaterialSet", "MaterialSetItem"):
if obj_type in ("Object", "MaterialSetItem"):
return context.active_object.name
return ""
@@ -197,7 +196,9 @@ def draw_psetqto_ui(
row = box.row(align=True)
row.scale_y = 0.8
row.label(text=prop["Name"])
op = row.operator("bim.select_similar", text=get_display_value(nominal_value), icon="NONE", emboss=False)
op = row.operator(
"bim.select_similar", text=get_display_value(nominal_value), icon="NONE", emboss=False
)
op.key = '"' + pset["Name"].replace('"', '\\"') + '"."' + prop["Name"].replace('"', '\\"') + '"'
# calculate sum of all selected objects
if active_operator:
@@ -400,10 +401,8 @@ class BIM_PT_material_psets(Panel):
if not ifc_file or ifc_file.schema == "IFC2X3":
return False # We don't support material psets in IFC2X3 because they suck
props = context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
return True
if props.is_editing and (material := props.active_material) and material.ifc_definition_id:
return True
return False
def draw(self, context):
@@ -430,46 +429,6 @@ class BIM_PT_material_psets(Panel):
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Material")
class BIM_PT_material_set_psets(Panel):
bl_label = "Material Set Property Sets"
bl_idname = "BIM_PT_material_set_psets"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_parent_id = "BIM_PT_object_material"
@classmethod
def poll(cls, context):
if not context.active_object:
return False
if not tool.Ifc.get() or tool.Ifc.get().schema == "IFC2X3":
return False # We don't support material psets in IFC2X3 because they suck
if not tool.Ifc.get_entity(context.active_object):
return False
if not ObjectMaterialData.is_loaded:
ObjectMaterialData.load()
ifc_class = ObjectMaterialData.data["material_class"]
return bool(ifc_class and "Set" in ifc_class)
def draw(self, context):
if not MaterialSetPsetsData.is_loaded:
MaterialSetPsetsData.load()
props = context.active_object.MaterialSetPsetProperties
row = self.layout.row(align=True)
prop_with_search(row, props, "pset_name", text="")
op = row.operator("bim.add_pset", icon="ADD", text="")
op.obj = context.active_object.name
op.obj_type = "MaterialSet"
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "PSET":
draw_psetqto_ui(context, 0, {}, props, self.layout, "MaterialSet")
for pset in MaterialSetPsetsData.data["psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "MaterialSet")
class BIM_PT_material_set_item_psets(Panel):
bl_label = "Material Set Item Property Sets"
bl_idname = "BIM_PT_material_set_item_psets"
@@ -532,19 +532,19 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
"material.assign_profile", tool.Ifc.get(), material_profile=material_profile, profile=profile
)
elif representation_template == "WINDOW":
with context.temp_override(active_object=obj):
with context.temp_override(active_object=obj, selected_objects=[]):
bpy.ops.bim.add_window()
elif representation_template == "DOOR":
with context.temp_override(active_object=obj):
with context.temp_override(active_object=obj, selected_objects=[]):
bpy.ops.bim.add_door()
elif representation_template == "STAIR":
with context.temp_override(active_object=obj):
with context.temp_override(active_object=obj, selected_objects=[]):
bpy.ops.bim.add_stair()
elif representation_template == "RAILING":
with context.temp_override(active_object=obj):
with context.temp_override(active_object=obj, selected_objects=[]):
bpy.ops.bim.add_railing()
elif representation_template == "ROOF":
with context.temp_override(active_object=obj):
with context.temp_override(active_object=obj, selected_objects=[]):
bpy.ops.bim.add_roof()
def draw(self, context):
@@ -20,6 +20,7 @@ import bpy
from . import ui, prop, operator, workspace
classes = (
operator.ShowLoads,
operator.LoadStructuralAnalysisModels,
operator.DisableStructuralAnalysisModelEditingUI,
operator.AddStructuralAnalysisModel,
@@ -83,6 +84,7 @@ classes = (
ui.BIM_PT_connected_structural_members,
ui.BIM_UL_structural_analysis_models,
ui.BIM_UL_structural_activities,
ui.BIM_PT_show_structural_activities,
ui.BIM_PT_structural_load_cases,
ui.BIM_UL_structural_loads,
ui.BIM_PT_structural_loads,
@@ -30,6 +30,50 @@ def refresh():
StructuralLoadCasesData.is_loaded = False
StructuralLoadsData.is_loaded = False
BoundaryConditionsData.is_loaded = False
LoadGroupDecorationData.is_loaded = False
class LoadGroupDecorationData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"load groups to show": cls.load_groups_to_show()}
cls.is_loaded = True
@classmethod
def load_groups_to_show(cls):
ret = []
abrv = {
"LOAD_CASE": "L.Case: ",
"LOAD_COMBINATION": "L.Comb: ",
"LOAD_GROUP": "L.Gr: ",
"USERDEFINED": "U.Def: ",
"NOTDEFINED": "N.Def: ",
}
models = tool.Ifc.get().by_type("IfcStructuralAnalysisModel")
m = models[0]
props = bpy.context.scene.BIMStructuralProperties
if props.activity_type == "Action":
groups = m.LoadedBy or []
for g in groups:
ret.append((str(g.id()), ". " + abrv[g.PredefinedType] + " " + g.Name, ""))
related_objects = [rel.RelatedObjects for rel in g.IsGroupedBy]
for item in related_objects:
for subgoup in [sg for sg in item if sg.is_a("IfcStructuralLoadGroup")]:
ret.append((str(subgoup.id()), ". " + abrv[subgoup.PredefinedType] + subgoup.Name, ""))
if props.activity_type == "External Reaction":
groups = m.HasResults or []
for g in groups:
result_name = g.ResultForLoadGroup.Name or ""
group_name = g.Name or ""
ret.append((str(g.id()), group_name + " " + result_name, ""))
if len(ret) == 0:
ret.append(("", "", ""))
return ret
class StructuralBoundaryConditionsData:
@@ -0,0 +1,156 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 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 mathutils import Vector
import numpy as np
import bpy
import gpu
import blf
from bpy.types import SpaceView3D
from gpu_extras.batch import batch_for_shader
from typing import Iterable, Union
from bonsai.bim.module.structural.load_decoration_data import ShaderInfo
class LoadsDecorator:
"""Decorator to show strucutural loads in 3D"""
is_installed = False
handlers = []
decoration_data = None
text_info = []
shader_info = []
depth_array = None
@classmethod
def install(cls, context: bpy.types.Context) -> None:
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(
SpaceView3D.draw_handler_add(handler.draw_load_values, ((context,)), "WINDOW", "POST_PIXEL")
)
cls.handlers.append(SpaceView3D.draw_handler_add(handler, (), "WINDOW", "POST_VIEW"))
cls.decoration_data = ShaderInfo()
cls.update()
cls.is_installed = True
@classmethod
def uninstall(cls) -> None:
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
except ValueError:
pass
cls.is_installed = False
@classmethod
def update(cls) -> None:
cls.decoration_data.update()
cls.text_info = cls.decoration_data.text_info
cls.shader_info = cls.decoration_data.info
def __call__(self) -> None:
"""set gpu configurations to draw 3D representations"""
# set open gl configurations
original_blend = gpu.state.blend_get()
original_depth_test = gpu.state.depth_test_get()
gpu.state.blend_set("ALPHA")
gpu.state.depth_test_set("LESS_EQUAL")
self.draw_batch()
# restore opengl configurations
gpu.state.blend_set(original_blend)
gpu.state.depth_test_set(original_depth_test)
def draw_batch(self) -> None:
"""draw the 3D representation of loads"""
if not self.decoration_data.is_empty:
for info in self.shader_info:
shader = info["shader"]
args = info["args"]
indices = info["indices"]
batch = batch_for_shader(shader, "TRIS", args, indices=indices)
matrix = bpy.context.region_data.perspective_matrix
shader.bind()
shader.uniform_float("viewProjectionMatrix", matrix)
for key, value in info["uniforms"]:
shader.uniform_float(key, value)
batch.draw(shader)
def draw_load_values(self, context: bpy.types.Context) -> None:
"""draw text representing the load values"""
# getting depth buffer info, code adapted from:
# https://blender.stackexchange.com/questions/177185/is-there-a-way-to-render-depth-buffer-into-a-texture-with-gpu-bgl-python-modules
framebuffer = gpu.state.active_framebuffer_get()
width = context.region.width
height = context.region.height
depth_buffer = framebuffer.read_depth(0, 0, width, height)
depth_array = np.array(depth_buffer.to_list())
f = context.area.spaces.active.clip_end
n = context.area.spaces.active.clip_start
self.depth_array = n / (f - (f - n) * depth_array) * (f - n)
for info in self.text_info:
text_position = self.location_3d_to_region_2d(info["position"], context)
if text_position is not None:
font_id = 0
blf.position(font_id, text_position[0], text_position[1], text_position[2])
blf.size(font_id, 20.0)
blf.color(font_id, 0.9, 0.9, 0.9, 1.0)
blf.draw(font_id, info["text"])
def location_3d_to_region_2d(self, coord: Iterable, context: bpy.types.Context) -> Union[Vector, None]:
"""Convert from 3D space to 2D screen space.
Filter out the text supposed to be hidden by 3D elements, using the depth array.
It also hides text that are distant from the camera view to avoid clutter"""
coord = Vector(coord)
rv3d = context.region_data
perspective = rv3d.view_perspective
view_matrix = rv3d.view_matrix
point_view_space = view_matrix @ coord
if perspective == "ORTHO" or -10 < point_view_space.z < 0:
prj = rv3d.perspective_matrix @ Vector((coord[0], coord[1], coord[2], 1.0))
width_half = context.region.width / 2.0
height_half = context.region.height / 2.0
coord_2d = Vector(
(
width_half + width_half * (prj.x / prj.w),
height_half + height_half * (prj.y / prj.w),
point_view_space.z,
)
)
if (
coord_2d[0] < 0
or coord_2d[0] > context.region.width
or coord_2d[1] > context.region.height
or coord_2d[1] < 0
):
return None
depth = self.depth_array[int(coord_2d[1])][int(coord_2d[0])]
if -0.98 * point_view_space.z > depth:
return None
return coord_2d
return None
@@ -0,0 +1,993 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 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/>.
import bpy
import bmesh
import numpy as np
from math import sin
from mathutils import Vector
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.attribute
import ifcopenshell.util.unit as ifcunit
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.structural.shader import DecorationShader
from typing import Literal, TypedDict, Iterable
MemberInfo = TypedDict(
"MemberInfo",
{"member": ifcopenshell.entity_instance, "activities": list[tuple[ifcopenshell.entity_instance, float]]},
)
LoadConfigItem = TypedDict(
"LoadConfigItem", {"pos": float, "descr": Literal["start", "end", "middle"], "load values": np.ndarray}
)
DiscreteConfigItem = TypedDict("DiscreteConfigItem", {"pos": float, "values": list[float]})
ParsedLoad = TypedDict(
"ParsedLoad",
{
"constant force": list[float],
"quadratic force": list[float],
"sinus force": list[float],
"linear load configuration": list[list[LoadConfigItem]],
"point load configuration": list[list[DiscreteConfigItem]],
},
)
LoadByDirection = TypedDict(
"LoadByDirection", {"constant": float, "quadratic": float, "sinus": float, "polyline": list[list[float]]}
)
ProcessedLoad = TypedDict(
"ProcessedLoad",
{"linear loads": LoadByDirection, "max linear load": float, "discrete loads": list[list[DiscreteConfigItem]]},
)
class ShaderInfo:
def __init__(self) -> None:
self.is_empty = True
self.shader = DecorationShader()
self.curve_members: dict[str, MemberInfo] = {}
self.point_members: dict[str, MemberInfo] = {}
self.surface_members: dict[str, MemberInfo] = {}
self.text_info = []
self.info = []
self.force_unit = ""
self.moment_unit = ""
self.linear_force_unit = ""
self.linear_moment_unit = ""
self.planar_force_unit = ""
def update(self) -> None:
self.info = []
self.text_info = []
self.curve_members = {}
self.point_members = {}
self.surface_members = {}
self.get_force_units()
self.get_strucutural_elements_and_activities()
self.get_linear_loads()
self.get_point_loads()
self.get_planar_loads()
if len(self.info):
self.is_empty = False
def get_force_units(self) -> None:
def get_unit_symbol(unit: ifcopenshell.entity_instance) -> str:
prefix_symbols = {
"EXA": "E",
"PETA": "P",
"TERA": "T",
"GIGA": "G",
"MEGA": "M",
"KILO": "k",
"HECTO": "h",
"DECA": "da",
"DECI": "d",
"CENTI": "c",
"MILLI": "m",
"MICRO": "μ",
"NANO": "n",
"PICO": "p",
"FEMTO": "f",
"ATTO": "a",
}
unit_symbols = {
# si units
"CUBIC_METRE": "m3",
"GRAM": "g",
"SECOND": "s",
"SQUARE_METRE": "m2",
"METRE": "m",
"NEWTON": "N",
"PASCAL": "Pa",
# conversion based units
"pound-force": "lbf",
"pound-force per square inch": "psi",
"thou": "th",
"inch": "in",
"foot": "ft",
"yard": "yd",
"mile": "mi",
"square thou": "th2",
"square inch": "in2",
"square foot": "ft2",
"square yard": "yd2",
"acre": "ac",
"square mile": "mi2",
"cubic thou": "th3",
"cubic inch": "in3",
"cubic foot": "ft3",
"cubic yard": "yd3",
"cubic mile": "mi3",
"litre": "L",
"fluid ounce UK": "fl oz",
"fluid ounce US": "fl oz",
"pint UK": "pt",
"pint US": "pt",
"gallon UK": "gal",
"gallon US": "gal",
"degree": "°",
"ounce": "oz",
"pound": "lb",
"ton UK": "ton",
"ton US": "ton",
"lbf": "lbf",
"kip": "kip",
"psi": "psi",
"ksi": "ksi",
"minute": "min",
"hour": "hr",
"day": "day",
"btu": "btu",
"fahrenheit": "°F",
}
symbol = ""
if unit.is_a("IfcSIUnit"):
symbol += prefix_symbols.get(unit.Prefix, "")
symbol += unit_symbols.get(unit.Name.replace("METER", "METRE"), "?")
return symbol
length_units = [u for u in tool.Ifc.get().by_type("IfcNamedUnit") if u.UnitType == "LENGTHUNIT"]
force_units = [u for u in tool.Ifc.get().by_type("IfcNamedUnit") if u.UnitType == "FORCEUNIT"]
linear_force_units = [u for u in tool.Ifc.get().by_type("IfcDerivedUnit") if u.UnitType == "LINEARFORCEUNIT"]
linear_moment_units = [u for u in tool.Ifc.get().by_type("IfcDerivedUnit") if u.UnitType == "LINEARMOMENTUNIT"]
planar_force_units = [u for u in tool.Ifc.get().by_type("IfcDerivedUnit") if u.UnitType == "PLANARFORCEUNIT"]
conversion_force_unit = [u for u in force_units if u.is_a("IfcConversionBasedUnit")]
if len(conversion_force_unit) == 0:
conversion_force_unit.append(force_units[0])
self.force_unit = ifcunit.get_unit_symbol(conversion_force_unit[0])
conversion_length_unit = [u for u in length_units if u.is_a("IfcConversionBasedUnit")]
if len(conversion_length_unit) == 0:
conversion_length_unit.append(length_units[0])
length_unit = ifcunit.get_unit_symbol(conversion_length_unit[0])
self.moment_unit = self.force_unit + "." + length_unit
first = ""
second = ""
for e in linear_force_units[0].Elements:
if e.Unit.UnitType == "FORCEUNIT":
first = ifcunit.get_unit_symbol(e.Unit)
if e.Unit.UnitType == "LENGTHUNIT":
second = ifcunit.get_unit_symbol(e.Unit)
self.linear_force_unit = first + "/" + second
first = ""
second = ""
for e in linear_moment_units[0].Elements:
if e.Unit.UnitType == "FORCEUNIT":
first = ifcunit.get_unit_symbol(e.Unit)
if e.Unit.UnitType == "LENGTHUNIT":
second = ifcunit.get_unit_symbol(e.Unit)
self.linear_moment_unit = first + "." + second + "/" + second
first = ""
second = ""
for e in planar_force_units[0].Elements:
if e.Unit.UnitType == "FORCEUNIT":
first = ifcunit.get_unit_symbol(e.Unit)
if e.Unit.UnitType == "LENGTHUNIT":
second = ifcunit.get_unit_symbol(e.Unit) + "2"
if e.Unit.UnitType == "AREAUNIT":
second = ifcunit.get_unit_symbol(e.Unit)
self.planar_force_unit = first + "/" + second
def get_strucutural_elements_and_activities(self) -> None:
"""fills self.point_members, self.curve_members and self.surface_members dictionaries"""
def populate_members_dict(
dict_name: Literal["point_members", "curve_members", "surface_members"],
element: ifcopenshell.entity_instance,
activity: ifcopenshell.entity_instance,
factor: float,
) -> None:
"""
fills self.point_members, self.curve_members and self.surface_members dictionaries
thoses dicts will contain the strucutural member global id as key and a second dict as value
the second dict contais two keys, as follow:
{
"member": the strucutral member itself
"activities: list[(activity, factor)] for each activity applied to the member
}
dict_name: "point_members", "curve_members" or "surface_members"
element: IfcStructuralMember
activity: IfcStructuralActivity
factor: float to multiply the loads values in the structural activity
"""
dic = getattr(self, dict_name, None)
if dic is None:
return
member = dic.get(element.GlobalId)
if member is None:
dic.update({element.GlobalId: {"member": element, "activities": [(activity, factor)]}})
else:
member["activities"].append((activity, factor))
def recursive_subgroups(
groups: list[ifcopenshell.entity_instance],
rec_limit: int,
activity_type: Literal["Action", "External Reaction"],
factor: float = 1,
) -> None:
"""
Recursively fills self.point_members, self.curve_members and self.surface_members dictionaries
with the structural members to wich the activities in the load group and its subgroups are applied
it also creates a list with all the activities applied to that member that are in the same
load group or subgroup (see populate_members_dict description)
groups: list of Ifc load case, load group or load combination
rec_limit: maximum number of recursions
activity_type: "Action" or "External Reaction"
factor: the factor applied to the group, default = 1
this factor will be multiplied by the group coefficient and the factor of a
IfcRelAssignsToGroupByFactor relationship of subgroups in load combinations
"""
if rec_limit == 0 or len(groups) == 0:
return None
for group in groups:
subgorups = []
activities = []
relationship = [rel for rel in group.IsGroupedBy]
coef = getattr(group, "Coefficient", 1.0)
group_coef = coef if coef is not None else 1.0
rel_factor = 1.0
for rel in relationship:
if rel.is_a("IfcRelAssignsToGroupByFactor"):
rel_factor = rel.Factor if rel.Factor is not None else 1.0
objects = rel.RelatedObjects
subgorups = [sg for sg in objects if sg.is_a("IfcStructuralLoadGroup")]
activities = [a for a in objects if a.is_a("IfcStructuralActivity")]
factor = factor * group_coef * rel_factor
for activity in activities:
if len(activity.AssignedToStructuralItem):
element = activity.AssignedToStructuralItem[0].RelatingElement
if element is not None:
if activity_type == "Action":
if element.is_a("IfcStructuralCurveMember"):
populate_members_dict("curve_members", element, activity, factor)
elif element.is_a("IfcStructuralPointConnection"):
populate_members_dict("point_members", element, activity, factor)
elif element.is_a("IfcStructuralSurfaceMember"):
populate_members_dict("surface_members", element, activity, factor)
elif (
activity_type == "External Reaction"
and getattr(element, "AppliedCondition", None) is not None
):
if element.is_a("IfcStructuralCurveMember"):
populate_members_dict("curve_members", element, activity, factor)
elif element.is_a("IfcStructuralPointConnection"):
populate_members_dict("point_members", element, activity, factor)
elif element.is_a("IfcStructuralSurfaceMember"):
populate_members_dict("surface_members", element, activity, factor)
recursive_subgroups(subgorups, rec_limit - 1, activity_type, factor=factor)
props = bpy.context.scene.BIMStructuralProperties
group_definition_id = int(props.load_group_to_show)
file = IfcStore.get_file()
groups = [file.by_id(group_definition_id)]
recursive_subgroups(groups, 10, props.activity_type)
def get_planar_loads(self) -> None:
"""get the necessary information to render the planar load representation in 3D and its text information"""
list_of_surfaces = self.surface_members
shader = self.shader.new("PLANAR LOAD")
maximum = 0
for value in list_of_surfaces.values():
surf = value["member"]
activity_list = value["activities"]
if len(activity_list) == 0:
continue
rotation = self.get_surface_member_rotation(surf)
values = self.get_planar_loads_values(activity_list, rotation)
if maximum == 0:
maximum = max([abs(float(i)) for i in values])
if maximum == 0:
continue
props = bpy.context.scene.BIMStructuralProperties
reference_frame = props.reference_frame
orientation = np.eye(3)
if reference_frame == "LOCAL_COORDS":
orientation = rotation
blender_object: bpy.types.Object = IfcStore.get_element(getattr(surf, "GlobalId", None))
mat = blender_object.matrix_world
mesh: bpy.types.Mesh = blender_object.data
positions = []
indices = []
coord = []
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.triangulate(bm, faces=bm.faces)
bm.edges.ensure_lookup_table()
bm.verts.ensure_lookup_table()
bm.faces.ensure_lookup_table()
for v in bm.verts:
p1 = np.array(mat @ v.co)
positions.append(p1)
p2 = p1 - (orientation @ values) * 0.2 / maximum
positions.append(p2)
coord.append((float(p1[0] + p1[1]), 0, 1))
coord.append((float(p1[0] + p1[1]), 1, 1))
for e in bm.edges:
if len(e.link_faces) > 1:
continue
indices.append((2 * e.verts[0].index, 2 * e.verts[0].index + 1, 2 * e.verts[1].index))
indices.append((2 * e.verts[0].index + 1, 2 * e.verts[1].index, 2 * e.verts[1].index + 1))
for p in bm.faces:
indices.append((2 * p.verts[0].index + 1, 2 * p.verts[1].index + 1, 2 * p.verts[2].index + 1))
bmesh.ops.dissolve_limit(bm, angle_limit=0.01, verts=bm.verts, edges=bm.edges)
bm.faces.ensure_lookup_table()
center = bm.faces[0].calc_center_bounds()
self.text_info.append(
{
"position": mat @ center - Vector((orientation @ values) * 0.2 / maximum),
"text": f"{values[2]:.5f} {self.planar_force_unit}",
}
)
self.info.append(
{
"shader": shader,
"args": {"position": positions, "coord": coord},
"indices": indices,
"uniforms": [["color", (0.2, 0, 1, 1)], ["spacing", 0.2]],
}
)
def get_planar_loads_values(
self, activity_list: list[tuple[ifcopenshell.entity_instance, float]], element_rotation_matrix: np.ndarray
) -> np.ndarray:
"""
returns a numpy array with the sum of the values of structural activities
applied loads in each direction, multiplied by the factors in load combinations
"""
values = np.zeros((3))
for item in activity_list:
activity = item[0]
factor = item[1]
load = activity.AppliedLoad
temp = np.zeros((3))
if load is not None and load.is_a("IfcStructuralLoadPlanarForce"):
temp[0] = load.PlanarForceX if load.PlanarForceX is not None else 0
temp[1] = load.PlanarForceY if load.PlanarForceY is not None else 0
temp[2] = load.PlanarForceZ if load.PlanarForceZ is not None else 0
temp = temp * factor
transform = self.get_activity_transform_matrix(activity, element_rotation_matrix)
values += transform @ temp
return values
def get_surface_member_rotation(self, surface_member: ifcopenshell.entity_instance) -> np.ndarray:
"""returns the rotation matrix of a structural surface member"""
representation = ifcopenshell.util.representation.get_representation(surface_member, "Model")
repr_item = representation.Items[0]
placement = ifcopenshell.util.placement.get_axis2placement(repr_item.FaceSurface.Position)
rotation = placement[0:3, 0:3]
return rotation
def get_point_connection_rotation(self, point_connection: ifcopenshell.entity_instance) -> np.ndarray:
"""returns the rotation matrix of a structural point connection"""
if point_connection.ConditionCoordinateSystem is not None:
placement = ifcopenshell.util.placement.get_axis2placement(point_connection.ConditionCoordinateSystem)
else:
placement = np.eye(4)
rotation = placement[0:3, 0:3]
return rotation
def get_curve_member_rotation(self, curve_member: ifcopenshell.entity_instance) -> np.ndarray:
"""returns the rotation matrix of a structural surface member"""
z = curve_member.Axis.DirectionRatios
edge = curve_member.Representation.Representations[0].Items[0]
origin = edge.EdgeStart.VertexGeometry.Coordinates
end = edge.EdgeEnd.VertexGeometry.Coordinates
x = [c2 - c1 for c1, c2 in zip(origin, end)]
placement = ifcopenshell.util.placement.a2p(origin, z, x)
rotation = placement[0:3, 0:3]
return rotation
def get_activity_transform_matrix(
self, activity: ifcopenshell.entity_instance, element_rotation_matrix: np.ndarray
) -> np.ndarray:
"provides the transformation matrix to convert between reference frames"
global_or_local = activity.GlobalOrLocal
props = bpy.context.scene.BIMStructuralProperties
reference_frame = props.reference_frame
transform_matrix = np.eye(3)
if reference_frame == "LOCAL_COORDS" and global_or_local != reference_frame:
transform_matrix = np.linalg.inv(element_rotation_matrix)
elif reference_frame == "GLOBAL_COORDS" and global_or_local != reference_frame:
transform_matrix = element_rotation_matrix
return transform_matrix
def get_point_loads(self) -> None:
"""get the necessary information to render the point load representation in 3D and its text information"""
list_of_point_connections = self.point_members
for value in list_of_point_connections.values():
conn = value["member"]
activity_list = value["activities"]
if len(activity_list) == 0:
continue
blender_object = IfcStore.get_element(getattr(conn, "GlobalId", None))
if blender_object.type == "MESH":
conn_location = blender_object.matrix_world @ blender_object.data.vertices[0].co
rotation = self.get_point_connection_rotation(conn)
loads = self.get_point_loads_values(activity_list, rotation)
self.get_point_shader_args(loads, conn_location, rotation)
def get_point_shader_args(self, loads: Iterable, location: np.ndarray, rotation: np.ndarray) -> None:
"""get the args to the point shader"""
location = np.array(location)
indices = []
direction_dict = {
"fx": (np.array((1, 0, 0)), np.array((0, 1, 0)), np.array((0, 0, 1))),
"fy": (np.array((0, 1, 0)), np.array((1, 0, 0)), np.array((0, 0, 1))),
"fz": (np.array((0, 0, 1)), np.array((0, 1, 0)), np.array((1, 0, 0))),
"mx": (np.array((0, 1, 0)), np.array((0, 0, 1))),
"my": (np.array((1, 0, 0)), np.array((0, 0, 1))),
"mz": (np.array((1, 0, 0)), np.array((0, 1, 0))),
}
keys = ["fx", "fy", "fz", "mx", "my", "mz"]
props = bpy.context.scene.BIMStructuralProperties
reference_frame = props.reference_frame
if reference_frame == "LOCAL_COORDS":
for key in keys:
tup = direction_dict[key]
li = []
for item in tup:
li.append(rotation @ item)
direction_dict[key] = li
for i, key in enumerate(keys):
if loads[i] == 0:
continue
color = (1, 0, 0, 1)
if i in [1, 4]:
color = (0, 1, 0, 1)
elif i in [2, 5]:
color = (0, 0, 1, 1)
d1 = -(direction_dict[key][0] * loads[i])
d1 = d1 / np.linalg.norm(d1)
if i < 3:
d2 = direction_dict[key][1]
d3 = direction_dict[key][2]
p1 = location
p2 = location + d1 + d2
p3 = location + d1 - d2
p4 = location + d1 + d3
p5 = location + d1 - d3
position = [p1, p2, p3, p4, p5]
indices = [(0, 1, 2), (0, 3, 4)]
c1 = (0, 0, 0)
c2 = (1, 1, 0)
c3 = (-1, 1, 0)
coords_for_shader = [c1, c2, c3, c2, c3]
shader = self.shader.new("SINGLE FORCE")
self.info.append(
{
"shader": shader,
"args": {"position": position, "coord": coords_for_shader},
"indices": indices,
"uniforms": [["color", color], ["spacing", 0.2]],
}
)
self.text_info.append({"position": location + d1, "text": f"{loads[i]:.2f} {self.force_unit}"})
else:
d2 = d2 = direction_dict[key][1]
p1 = location - d2
p2 = location + d1 + d2
p3 = location - d1 + d2
position = [p1, p2, p3]
indices = [(0, 1, 2)]
c1 = (-1, 0, 0)
c2 = (1, 1, 0)
c3 = (1, -1, 0)
coords_for_shader = [c1, c2, c3]
shader = self.shader.new("SINGLE MOMENT")
self.info.append(
{
"shader": shader,
"args": {"position": position, "coord": coords_for_shader},
"indices": indices,
"uniforms": [["color", color]],
}
)
self.text_info.append(
{"position": location + 0.25 * (d1 + d2), "text": f"{loads[i]:.2f} {self.moment_unit}"}
)
def get_point_loads_values(
self, activity_list: list[tuple[ifcopenshell.entity_instance, float]], element_rotation_matrix: np.ndarray
) -> np.ndarray:
"""returns a numpy array with the sum of the point load values"""
result_list = np.zeros(6)
attr_list = ["ForceX", "ForceY", "ForceZ", "MomentX", "MomentY", "MomentZ"]
for item in activity_list:
activity = item[0]
factor = item[1]
load = activity.AppliedLoad
temp = np.zeros(6)
for i, attr in enumerate(attr_list):
value = 0 if getattr(load, attr, 0) is None else getattr(load, attr, 0)
temp[i] += value * factor
transform_3 = self.get_activity_transform_matrix(activity, element_rotation_matrix)
transform_6 = np.zeros((6, 6))
transform_6[0:3, 0:3] = transform_3
transform_6[3:6, 3:6] = transform_3
result_list += transform_6 @ temp
return result_list
def get_linear_loads(self) -> None:
position = []
indices = []
sin_quad_lin = []
coords_for_shader = []
color = []
info = []
maxforce = 0
list_of_curve_members = self.curve_members
for value in list_of_curve_members.values():
member = value["member"]
activity_list = value["activities"]
if len(activity_list) == 0:
continue
blender_object = IfcStore.get_element(getattr(member, "GlobalId", None))
start_co = blender_object.matrix_world @ blender_object.data.vertices[0].co
end_co = blender_object.matrix_world @ blender_object.data.vertices[1].co
x_axis = Vector(end_co - start_co).normalized()
z_direction = getattr(member, "Axis")
# local coordinates
z_axis = Vector(getattr(z_direction, "DirectionRatios", None)).normalized()
y_axis = z_axis.cross(x_axis).normalized()
z_axis = x_axis.cross(y_axis).normalized()
rotation = self.get_curve_member_rotation(member)
props = bpy.context.scene.BIMStructuralProperties
reference_frame = props.reference_frame
is_local = reference_frame == "LOCAL_COORDS"
x_match = abs(Vector((1, 0, 0)).dot(x_axis)) > 0.99
y_match = abs(Vector((0, 1, 0)).dot(x_axis)) > 0.99
z_match = abs(Vector((0, 0, 1)).dot(x_axis)) > 0.99
direction_dict = {
"fx": y_axis + z_axis if is_local else Vector((1, 0, 0)) if not x_match else Vector((0, 1, 1)),
"fy": y_axis if is_local else Vector((0, 1, 0)) if not y_match else Vector((1, 0, 1)),
"fz": z_axis if is_local else Vector((0, 0, 1)) if not z_match else Vector((1, 1, 0)),
"mx": z_axis - y_axis if is_local or x_match else Vector((1, 0, 0)).cross(x_axis),
"my": (
z_axis
if is_local
else Vector((-1, 0, 1)) if y_match else Vector((0, 1, 0)).cross(x_axis).normalized()
),
"mz": (
y_axis
if is_local
else Vector((-1, 1, 0)) if z_match else Vector((0, 0, 1)).cross(x_axis).normalized()
),
}
match_dict = {"fx": x_match or is_local, "fy": y_match, "fz": z_match}
member_length = Vector(end_co - start_co).length
processed_loads = self.process_total_linear_loads(activity_list, rotation, member_length)
linear_loads = processed_loads["linear loads"]
maxforce = max(maxforce, processed_loads["max linear load"])
point_loads = processed_loads["discrete loads"]
if len(point_loads) > 0:
for item in point_loads:
for sub_item in item:
pos = sub_item["pos"]
values = sub_item["values"]
pos_vector = start_co + x_axis * pos
self.get_point_shader_args(values, pos_vector, rotation)
if linear_loads is None:
continue
keys = ["fx", "fy", "fz", "mx", "my", "mz"]
for key in keys:
polyline = linear_loads[key]["polyline"]
sinus = linear_loads[key]["sinus"]
quadratic = linear_loads[key]["quadratic"]
constant = linear_loads[key]["constant"]
direction = direction_dict[key]
color_axis = (0, 0, 1, 1)
if "x" in key:
color_axis = (1, 0, 0, 1)
if "y" in key:
color_axis = (0, 1, 0, 1)
if "f" in key:
unit = self.linear_force_unit
if match_dict[key]:
shader = self.shader.new("PARALLEL DISTRIBUTED FORCE")
else:
shader = self.shader.new("PERPENDICULAR DISTRIBUTED FORCE")
else:
unit = self.linear_moment_unit
shader = self.shader.new("DISTRIBUTED MOMENT")
counter = 0
for i in range(len(polyline) - 1):
current = Vector(polyline[i] + [0])
nextitem = Vector(polyline[i + 1] + [0])
if any([current.y, nextitem.y, constant, quadratic, sinus]):
negative = -1 * direction + start_co + x_axis * current.x
positive = direction + start_co + x_axis * current.x
position.append(negative)
coords_for_shader.append((current.x, 1.0, member_length))
sin_quad_lin.append((sinus, quadratic, current.y + constant))
color.append(color_axis)
x = current.x / member_length
func = sin(x * 3.1416) * sinus + (-4.0 * x * x + 4.0 * x) * quadratic + constant + current.y
if func:
self.text_info.append(
{
"position": -1 * direction * func / maxforce + start_co + x_axis * current.x,
"text": f"{func:.2f} {unit}",
}
)
position.append(positive)
coords_for_shader.append((current[0], -1.0, member_length))
sin_quad_lin.append((sinus, quadratic, current.y + constant))
color.append(color_axis)
indices.append((0 + counter, 1 + counter, 2 + counter))
indices.append((3 + counter, 2 + counter, 1 + counter))
if i == len(polyline) - 2:
negative = -1 * direction + start_co + x_axis * nextitem.x
positive = direction + start_co + x_axis * nextitem.x
position.append(negative)
coords_for_shader.append((nextitem.x, 1.0, member_length))
sin_quad_lin.append((sinus, quadratic, nextitem.y + constant))
color.append(color_axis)
x = nextitem.x / member_length
func = (
sin(x * 3.1416) * sinus + (-4.0 * x * x + 4.0 * x) * quadratic + constant + nextitem.y
)
if func:
self.text_info.append(
{
"position": -1 * direction * func / maxforce + start_co + x_axis * nextitem.x,
"text": f"{func:.2f} {unit}",
}
)
position.append(positive)
coords_for_shader.append((nextitem.x, -1.0, member_length))
sin_quad_lin.append((sinus, quadratic, nextitem.y + constant))
color.append(color_axis)
counter += 2
if position:
self.info.append(
{
"shader": shader,
"args": {
"position": position,
"sin_quad_lin_forces": sin_quad_lin,
"coord": coords_for_shader,
},
"indices": indices,
"uniforms": [["color", color_axis], ["spacing", 0.2], ["maxload", maxforce]],
}
)
position = []
sin_quad_lin = []
coords_for_shader = []
indices = []
for info in self.info:
info["uniforms"][2][1] = maxforce
def process_total_linear_loads(
self,
activity_list: list[tuple[ifcopenshell.entity_instance, float]],
element_rotation_matrix: np.ndarray,
member_length: float,
) -> ProcessedLoad:
"""returns a dict with total values for applied loads in each direction
along with the maximum value for the loads in the member and the discrete loads applied
"""
loads_dict = self.parse_linear_loads_to_dict(activity_list, element_rotation_matrix)
const = loads_dict["constant force"]
quad = loads_dict["quadratic force"]
sinus = loads_dict["sinus force"]
loads = loads_dict["linear load configuration"]
unique_list = self.getuniquepositionlist(loads)
final_list = []
distributed_loads = None
max_load = 0
for pos in unique_list:
value = self.get_before_and_after(pos, loads)
if value["before"] == value["after"]:
final_list.append([pos] + value["before"])
else:
final_list.append([pos] + value["before"])
final_list.append([pos] + value["after"])
if len(final_list) == 0 and any(const + quad + sinus):
final_list.append([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
final_list.append([member_length, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
elif len(final_list) > 0:
if final_list[0][0] and any(
const + quad + sinus
): # if first item location is not 0 append an item at the zero
final_list = [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]] + final_list
else:
del final_list[0]
if abs(final_list[-1][0] - member_length) > 0.01 and any(const + quad + sinus):
final_list.append([member_length, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
else:
del final_list[-1]
if len(final_list) > 0:
array = np.array(final_list) # 7xn -> ["pos","fx","fy","fz","mx","my","mz"]
keys = ["fx", "fy", "fz", "mx", "my", "mz"]
polyline = []
distributed_loads = {
"fx": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
"fy": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
"fz": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
"mx": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
"my": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
"mz": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
}
max_load = 0
for component, key in enumerate(keys):
if any([sinus[component], quad[component], const[component]]) or any(
item for item in array[:, component + 1]
):
for currentitem in final_list:
polyline.append([currentitem[0], currentitem[component + 1]])
max_load = max(
max_load,
abs(sinus[component] + quad[component] + const[component] + currentitem[component + 1]),
)
inner_dict = distributed_loads[key]
inner_dict["constant"] = const[component]
inner_dict["quadratic"] = quad[component]
inner_dict["sinus"] = sinus[component]
inner_dict["polyline"] = polyline
distributed_loads[key] = inner_dict
return {
"linear loads": distributed_loads,
"max linear load": max_load,
"discrete loads": loads_dict["point load configuration"],
}
def getuniquepositionlist(self, load_config_list: list[list[LoadConfigItem]]) -> list[float]:
"""return an ordereded list of unique locations based on the load configuration list
ex: load_config_list = [[{"pos":1.0,...},{"pos":3.0,...}],
[{"pos":2.0,...},{"pos":3.0,...}],
[{"pos":1.5,...},{"pos":2.5,...}]]
return = [1.0, 1.5, 2.0, 2.5, 3.0]
"""
unique = []
for config in load_config_list:
for info in config:
if info["pos"] in unique:
continue
unique.append(info["pos"])
unique.sort()
return unique
def interp1d(self, l1: list[float], l2: list[float], pos: float) -> float:
"""1d linear interpolation for the vector components"""
fac = (l2[1] - l1[1]) / (l2[0] - l1[0])
v = l1[1] + fac * (pos - l1[0])
return v
def interpolate(self, pos: float, loadinfo: list[LoadConfigItem], start: int, end: int, key: str) -> np.ndarray:
"""interpolate the result vectors between load poits"""
result = np.zeros(6)
for i in range(6):
value1 = [loadinfo[start]["pos"], loadinfo[start][key][i]] # [position, force_component]
value2 = [loadinfo[end]["pos"], loadinfo[end][key][i]] # [position, force_component]
result[i] = self.interp1d(value1, value2, pos) # interpolated [position, force_component]
return result
def get_before_and_after(self, pos: float, load_config_list: list[list[LoadConfigItem]]) -> dict[str, list[float]]:
"""get total values for forces and moments before and after the position
example:
pos = 2.0
load_config_list = [[{"pos":1.0, "descr":"start, "load values":[1,0,0,0,0,0]},
{"pos":3.0, "descr":"end, "load values":[3,0,0,0,0,0]}],
[{"pos":2.0, "descr":"start, "load values":[1,0,0,0,0,0]},
{"pos":3.0, "descr":"end, "load values":[1,0,0,0,0,0]}],
[{"pos":1.5, "descr":"start, "load values":[1,0,0,0,0,0]},
{"pos":2.5, "descr":"end, "load values":[1,0,0,0,0,0]}],
return = {
"before": [3,0,0,0,0,0], ->(fx, fy, fz, mx, my, mz)
" after": [4,0,0,0,0,0] ->(fx, fy, fz, mx, my, mz)
}
"""
load_before = np.zeros(6)
load_after = np.zeros(6)
for config in load_config_list:
if pos < config[0]["pos"] or pos > config[-1]["pos"]:
continue
start = 0
end = len(config) - 1
while end - start > 0:
if pos < config[start]["pos"] or pos > config[end]["pos"]:
break
if config[start]["pos"] == pos:
if config[start]["descr"] in ["start", "middle"]:
load_after += config[start]["load values"]
elif config[start]["descr"] in ["end", "middle"]:
load_before += config[start]["load values"]
elif config[end]["pos"] == pos:
if config[end]["descr"] in ["start", "middle"]:
load_after += config[end]["load values"]
elif config[end]["descr"] in ["end", "middle"]:
load_before += config[end]["load values"]
elif end - start == 1:
load_before += self.interpolate(pos, config, start, end, "load values")
load_after += self.interpolate(pos, config, start, end, "load values")
start += 1
end -= 1
return_value = {"before": load_before.tolist(), "after": load_after.tolist()}
return return_value
def parse_linear_loads_to_dict(
self, activity_list: list[tuple[ifcopenshell.entity_instance, float]], element_rotation_matrix: np.ndarray
) -> ParsedLoad:
"""
get load list
activity_list: list of IfcStructuralCurveAction or IfcStructuralCurveReaction
applied in the structural curve member
global_to_local: transformation matrix from global coordinates to local coordinetes
return: dict{
"constant force": (fx,fy,fz,mx,my,mz), -> sum of linear loads applied with
constant distribution
"quadratic force": (fx,fy,fz,mx,my,mz), -> sum of linear loads applied with
quadratic distribution
"sinus force": (fx,fy,fz,mx,my,mz), -> sum of linear loads applied with
sinus distribution
"linear load configuration": list -> list of load configurations for linear
and polyline distributions of linear loads
}
description of "linear load configuration":
list[ -> one item (list)for each IfcStructuralCurveAction applied in the member
with IfcStructuralLoadConfiguration as the applied load
list[ -> one item (dict) for each item found in the
Locations attribute of IfcLoadConfiguration
dict{
"pos": float, -> local position along curve length
"descr": string, -> describe if the item is at the start, middle or end of the list
"load values": Array, -> linear force applied at that point
}
]
]
"""
constant = np.zeros(6)
quadratic = np.zeros(6)
sinus = np.zeros(6)
linear_load_configurations = []
point_load_configurations = []
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get(), "LENGTHUNIT")
def get_load_values(load, transform_matrix, factor=1.0):
result = np.zeros(6)
keys = ["LinearForceX", "LinearForceY", "LinearForceZ", "LinearMomentX", "LinearMomentY", "LinearMomentZ"]
for i, key in enumerate(keys):
value = 0 if getattr(load, key, 0) is None else getattr(load, key, 0)
result[i] += value * factor
return transform_matrix @ result
for item in activity_list:
activity = item[0]
factor = item[1]
load = activity.AppliedLoad
transform_3by3 = self.get_activity_transform_matrix(activity, element_rotation_matrix)
transform_6by6 = np.zeros((6, 6))
transform_6by6[0:3, 0:3] = transform_3by3
transform_6by6[3:6, 3:6] = transform_3by3
# values for linear loads
if load.is_a("IfcStructuralLoadConfiguration"):
locations = getattr(load, "Locations", [])
values = [l for l in getattr(load, "Values", None) if l.is_a() == "IfcStructuralLoadLinearForce"]
config_list = []
for i, l in enumerate(values):
load_values = get_load_values(l, transform_6by6, factor)
if i == 0:
descr = "start"
elif i == len(values) - 1:
descr = "end"
else:
descr = "middle"
config_list.append(
{"pos": locations[i][0] * unit_scale, "descr": descr, "load values": load_values}
)
linear_load_configurations.append(config_list)
# load configurations with point loads
values = [l for l in getattr(load, "Values", None) if l.is_a() == "IfcStructuralLoadSingleForce"]
attr_list = ["ForceX", "ForceY", "ForceZ", "MomentX", "MomentY", "MomentZ"]
config_list = []
for i, val in enumerate(values):
result_list = [0, 0, 0, 0, 0, 0]
for j, attr in enumerate(attr_list):
value = 0 if getattr(val, attr, 0) is None else getattr(val, attr, 0)
result_list[j] += value * factor
config_list.append({"pos": locations[i][0] * unit_scale, "values": result_list})
point_load_configurations.append(config_list)
else:
load_values = get_load_values(load, transform_6by6, factor)
if "CONST" == getattr(activity, "PredefinedType", None) or activity.is_a("IfcStructuralLinearAction"):
constant += load_values
elif "PARABOLA" == getattr(activity, "PredefinedType", None):
quadratic += load_values
elif "SINUS" == getattr(activity, "PredefinedType", None):
sinus += load_values
return_value = {
"constant force": constant.tolist(),
"quadratic force": quadratic.tolist(),
"sinus force": sinus.tolist(),
"linear load configuration": linear_load_configurations,
"point load configuration": point_load_configurations,
}
return return_value
@@ -27,6 +27,45 @@ import bonsai.tool as tool
from math import degrees
from mathutils import Vector, Matrix
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.structural.decorator import LoadsDecorator
class ShowLoads(bpy.types.Operator):
"""Draw decorations to show strucutural actions in 3d view"""
bl_idname = "bim.show_loads"
bl_label = "Show loads in 3D View"
def modal(self, context, event):
if event.type == "F5":
LoadsDecorator.update()
for area in context.screen.areas:
if area.type == "VIEW_3D":
area.tag_redraw()
if event.type == "ESC":
LoadsDecorator.uninstall()
for area in context.screen.areas:
if area.type == "VIEW_3D":
area.tag_redraw()
return {"FINISHED"}
return {"PASS_THROUGH"}
def invoke(self, context, event):
collection = bpy.data.collections["IfcStructuralItem"]
collection.hide_viewport = False
context.window.cursor_modal_set("WAIT")
try:
LoadsDecorator.install(context)
except Exception as exc:
context.window.cursor_modal_restore()
raise exc
context.window.cursor_modal_restore()
context.window_manager.modal_handler_add(self)
for area in context.screen.areas:
if area.type == "VIEW_3D":
area.tag_redraw()
return {"RUNNING_MODAL"}
class AddStructuralMemberConnection(bpy.types.Operator, tool.Ifc.Operator):
@@ -21,7 +21,12 @@ import bpy
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty, Attribute
from bonsai.bim.module.structural.data import StructuralLoadCasesData, StructuralLoadsData, BoundaryConditionsData
from bonsai.bim.module.structural.data import (
StructuralLoadCasesData,
StructuralLoadsData,
BoundaryConditionsData,
LoadGroupDecorationData,
)
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -35,6 +40,16 @@ from bpy.props import (
)
def get_load_groups_to_show(self, context):
if not LoadGroupDecorationData.is_loaded:
LoadGroupDecorationData.load()
return LoadGroupDecorationData.data["load groups to show"]
def update_activity_type(self, context):
LoadGroupDecorationData.is_loaded = False
def get_applicable_structural_load_types(self, context):
if not StructuralLoadCasesData.is_loaded:
StructuralLoadCasesData.load()
@@ -152,6 +167,27 @@ class BIMStructuralProperties(PropertyGroup):
boundary_condition_attributes: CollectionProperty(name="Boundary Condition Attributes", type=Attribute)
filtered_boundary_conditions: BoolProperty(name="Filtered Boundary Conditions", default=False)
show_loads: BoolProperty(name="Show Loads", default=False)
update_load_repr: BoolProperty(name="Update Load Representation", default=False)
enable_repr_auto_update: BoolProperty(name="Auto Update", default=False)
reference_frame: EnumProperty(
items=[
("GLOBAL_COORDS", "Global", "Show loads in global reference frame"),
("LOCAL_COORDS", "Local", "Show loads in local reference frame"),
],
name="Reference Frame",
)
activity_type: EnumProperty(
items=[
("Action", "Actions", "Show actions loads"),
("External Reaction", "External Reactions", "Show reactions on boundary conditions"),
("Internal Reactions", "Internal Reactions", "Show internal reactions on members"),
],
name="Activity Type",
update=update_activity_type,
)
load_group_to_show: EnumProperty(items=get_load_groups_to_show, name="Load Groups")
class BIMObjectStructuralProperties(PropertyGroup):
boundary_condition_attributes: CollectionProperty(name="Boundary Condition Attributes", type=Attribute)
@@ -0,0 +1,294 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 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/>.
import gpu
from typing import Literal
class DecorationShader:
"shader for the load decorations"
def __init__(self):
pass
def new(
self,
pattern: Literal[
"PERPENDICULAR DISTRIBUTED FORCE",
"PARALLEL DISTRIBUTED FORCE",
"DISTRIBUTED MOMENT",
"SINGLE FORCE",
"SINGLE MOMENT",
"PLANAR LOAD",
],
) -> gpu.types.GPUShader:
"""pattern: string description of the desired shader
Possible values
PERPENDICULAR DISTRIBUTED FORCE: pattern for distributed force
perpendicular to the curve member axis
PARALLEL DISTRIBUTED FORCE: pattern for distributed force
along the curve member axis
DISTRIBUTED MOMENT: pattern for distributed moment in curve members
SINGLE FORCE: pattern for single forces
SINGLE MOMENT: pattern for single moments
PLANAR LOAD: pattern for planar loads
"""
valid_patterns = {
"PERPENDICULAR DISTRIBUTED FORCE",
"PARALLEL DISTRIBUTED FORCE",
"DISTRIBUTED MOMENT",
"SINGLE FORCE",
"SINGLE MOMENT",
"PLANAR LOAD",
}
if pattern not in valid_patterns:
raise ValueError(
"""pattern must be one of:
PERPENDICULAR DISTRIBUTED FORCE
PARALLEL DISTRIBUTED FORCE,
DISTRIBUTED MOMENT,
SINGLE FORCE,
SINGLE MOMENT,
PLANAR LOAD"""
)
if "DISTRIBUTED" in pattern.upper():
shader = self.get_linear_shader(pattern)
return shader
elif "SINGLE" in pattern.upper():
shader = self.get_point_shader(pattern)
return shader
elif "PLANAR" in pattern.upper():
shader = self.get_planar_shader()
return shader
def get_linear_shader(
self, pattern: Literal["PERPENDICULAR DISTRIBUTED FORCE", "PARALLEL DISTRIBUTED FORCE", "DISTRIBUTED MOMENT"]
) -> gpu.types.GPUShader:
"""pattern: type of pattern
PERPENDICULAR DISTRIBUTED FORCE
PARALLEL DISTRIBUTED FORCE,
DISTRIBUTED MOMENT,
"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth("VEC3", "forces")
vert_out.smooth("VEC3", "co")
shader_info = gpu.types.GPUShaderCreateInfo()
shader_info.push_constant("MAT4", "viewProjectionMatrix")
shader_info.push_constant("VEC4", "color")
shader_info.push_constant("FLOAT", "spacing")
shader_info.push_constant("FLOAT", "maxload")
shader_info.vertex_in(0, "VEC3", "position")
shader_info.vertex_in(1, "VEC3", "sin_quad_lin_forces")
shader_info.vertex_in(2, "VEC3", "coord")
shader_info.vertex_out(vert_out)
shader_info.fragment_out(0, "VEC4", "FragColor")
shader_info.vertex_source(
"void main()"
"{"
" gl_Position = viewProjectionMatrix * vec4(position, 1.0f);"
" co = coord;"
" forces = sin_quad_lin_forces;"
"}"
)
if pattern == "PERPENDICULAR DISTRIBUTED FORCE":
shader_info.fragment_source(
"void main()"
"{"
"float x = co.x;"
"float y = co.y;"
"float abs_y = abs(y);"
"float a = abs(mod(x,spacing)-0.5*spacing)*5.0;"
"float b = step(a,abs_y)*(step(abs_y,1.2*spacing));"
"float c = step(0.8*spacing,mod(x+0.4*spacing,spacing))*(step(1.2*spacing,abs_y));"
"float sinvalue = forces.x;"
"float quadraticvalue = forces.y;"
"float linearvalue = forces.z;"
"x = co.x/co.z;"
"float f = (sin(x*3.1416)*sinvalue"
"+(-4.*x*x+4.*x)*quadraticvalue"
"+linearvalue)/maxload;"
"float mask = step(0.,y)*step(y,f)+step(y,0.)*step(f,y);"
"float top = step(abs(y-f),0.2*1.2*spacing);"
"float d = clamp(top+b+c,0.0,0.9)*mask;"
"if (d == 0.0) discard;"
"FragColor = vec4(color.xyz,d*color.w);"
"}"
)
elif pattern == "PARALLEL DISTRIBUTED FORCE":
shader_info.fragment_source(
"void main()"
"{"
"float y = co.y;"
"float x = step(0.,y)*(co.z-co.x)+step(y,0.)*(co.x);"
"float abs_y = abs(y);"
"float a = abs(mod(abs_y,spacing)-0.5*spacing)*5.0;"
"float a2 = mod(x,3.0*spacing);"
"float b = step(a,a2)*step(a2,1.2*spacing);"
"float c = step(0.8*spacing,mod(abs_y+0.4*spacing,spacing))"
"*(step(1.2*spacing,a2))*step(a2,2.5*spacing);"
"float sinvalue = forces.x;"
"float quadraticvalue = forces.y;"
"float linearvalue = forces.z;"
"x = co.x/co.z;"
"float f = (sin(x*3.1416)*sinvalue"
"+(-4.*x*x+4.*x)*quadraticvalue"
"+linearvalue)/maxload;"
"float mask = step(0.,y)*step(y,f)+step(y,0.)*step(f,y);"
"float top = step(abs(y-f),0.2*1.2*spacing);"
"float d = clamp(top+b+c,0.0,0.9)*mask;"
"if (d == 0.0) discard;"
"FragColor = vec4(color.xyz,d*color.w);"
"}"
)
elif pattern == "DISTRIBUTED MOMENT":
shader_info.fragment_source(
"void main()"
"{"
"float x = step(co.y,0.)*(co.x)+step(0.,co.y)*(co.z-co.x);"
"float y = step(co.y,-0.00001)*(co.y)+step(0.,co.y)*(0.-co.y);"
"x = mod((0.5/spacing)*x,1.4)-0.7;"
"y = mod((0.5/spacing)*y,1.4)-0.7;"
"float abs_y = abs(y);"
"vec2 st = vec2(1.9*x,y);"
"vec2 orig = vec2(0.,0.);"
"float circ = step(distance(st,orig),0.33)*step(0.27,distance(st,orig));"
"float tri_mask = step(st.y,st.x)+step(-st.x,st.y);"
"float circ_arrow = step(st.x,4.0*st.y-0.75)*step(0.25*st.y-0.34,st.x)*(1.-tri_mask);"
"float circmask = step(distance(st,orig),0.1)+step(0.5,distance(st,orig))+step(st.x,0.);"
"float body = step(-0.03,st.y)*step(st.y,0.03)*step(-0.3,x)*step(x,0.576);"
"float body_arrow = step(-0.5+3.*st.y,x)*step(-0.5-3.*st.y,x)*step(x,-0.3);"
"float d = clamp(circmask*(body+body_arrow)+circ_arrow+circ*tri_mask,0.,1.);"
"float sinvalue = forces.x;"
"float quadraticvalue = forces.y;"
"float linearvalue = forces.z;"
"x = co.x/co.z;"
"float f = (sin(x*3.1416)*sinvalue"
"+(-4.*x*x+4.*x)*quadraticvalue"
"+linearvalue)/maxload;"
"float mask = step(0.,co.y)*step(co.y,f)+step(co.y,0.)*step(f,co.y);"
"float top = step(abs(co.y-f),0.2*1.2*spacing);"
"d = clamp(top+d,0.0,0.9)*mask;"
"if (d == 0.0) discard;"
"FragColor = vec4(color.xyz,d*color.w);"
"}"
)
shader = gpu.shader.create_from_info(shader_info)
del vert_out
del shader_info
return shader
def get_point_shader(self, pattern: Literal["SINGLE FORCE", "SINGLE MOMENT"]) -> gpu.types.GPUShader:
"""param: pattern: type of pattern
SINGLE FORCE,
SINGLE MOMENT"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth("VEC3", "co")
shader_info = gpu.types.GPUShaderCreateInfo()
shader_info.push_constant("MAT4", "viewProjectionMatrix")
shader_info.push_constant("VEC4", "color")
shader_info.push_constant("FLOAT", "spacing")
shader_info.vertex_in(0, "VEC3", "position")
shader_info.vertex_in(1, "VEC3", "coord")
shader_info.vertex_out(vert_out)
shader_info.fragment_out(0, "VEC4", "FragColor")
shader_info.vertex_source(
"void main()" "{" " gl_Position = viewProjectionMatrix * vec4(position, 1.0f);" " co = coord;" "}"
)
if pattern == "SINGLE FORCE":
shader_info.fragment_source(
"void main()"
"{"
"float body = step(abs(co.x),0.2*spacing)*step(2.*spacing,co.y);"
"float arrow = step(3.5*abs(co.x)+0.02,abs(co.y))*step(co.y,2.*spacing)*step(0.,co.y);"
"float d = clamp(body+arrow,0.0,0.5);"
"if (d == 0.0) discard;"
"FragColor = vec4(color.xyz,d*color.w);"
"}"
)
elif pattern == "SINGLE MOMENT":
shader_info.fragment_source(
"void main()"
"{"
"float circ = step(distance(co.xy,vec2(0.,0.)),0.33)*step(0.27,distance(co.xy,vec2(0.,0.)));"
"float mask = step(co.y,co.x)+step(-co.x,co.y);"
"float circ_arrow = step(co.x,4.0*co.y-0.75)*step(0.25*co.y-0.34,co.x)*(1.-mask);"
"float d = clamp(circ_arrow+circ*mask,0.0,0.5);"
"if (d == 0.0) discard;"
"FragColor = vec4(color.xyz,d*color.w);"
"}"
)
shader = gpu.shader.create_from_info(shader_info)
del vert_out
del shader_info
return shader
def get_planar_shader(self) -> gpu.types.GPUShader:
"""shader for planar loads"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth("VEC3", "co")
shader_info = gpu.types.GPUShaderCreateInfo()
shader_info.push_constant("MAT4", "viewProjectionMatrix")
shader_info.push_constant("VEC4", "color")
shader_info.push_constant("FLOAT", "spacing")
shader_info.vertex_in(0, "VEC3", "position")
shader_info.vertex_in(1, "VEC3", "coord")
shader_info.vertex_out(vert_out)
shader_info.fragment_out(0, "VEC4", "FragColor")
shader_info.vertex_source(
"void main()" "{" " gl_Position = viewProjectionMatrix * vec4(position, 1.0f);" " co = coord;" "}"
)
shader_info.fragment_source(
"void main()"
"{"
"float x = co.x;"
"float y = co.y;"
"float abs_y = abs(y);"
"float a = abs(mod(x,spacing)-0.5*spacing)*5.0;"
"float b = step(a,abs_y)*(step(abs_y,1.2*spacing));"
"float c = step(0.8*spacing,mod(x+0.4*spacing,spacing))*(step(1.2*spacing,abs_y));"
"float mask = step(0.,y)*step(y,0.98)+step(y,0.)*step(0.98,y);"
"float top = step(abs(y-0.98),0.2*1.2*spacing);"
"float d = clamp(0.2*y+(top+b+c)*mask,0.0,0.4);"
"FragColor = vec4(color.xyz,d*color.w);"
"}"
)
shader = gpu.shader.create_from_info(shader_info)
del vert_out
del shader_info
return shader
@@ -445,6 +445,37 @@ class BIM_UL_structural_activities(UIList):
row.label(text=item.applied_load_class)
class BIM_PT_show_structural_activities(Panel):
bl_label = "Show Loads"
bl_idname = "BIM_PT_show_structural_activities"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_structural"
@classmethod
def poll(cls, context):
file = IfcStore.get_file()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
self.props = context.scene.BIMStructuralProperties
row = self.layout.row(align=True)
row.operator(
"bim.show_loads",
text="Show loads",
icon="HIDE_OFF",
)
row = self.layout.row(align=True)
row.prop(self.props, "reference_frame")
row = self.layout.row(align=True)
row.prop(self.props, "activity_type")
row = self.layout.row(align=True)
row.prop(self.props, "load_group_to_show")
class BIM_PT_structural_loads(Panel):
bl_label = "Structural Loads"
bl_idname = "BIM_PT_structural_loads"
@@ -80,7 +80,6 @@ class RemoveStyle(bpy.types.Operator, tool.Ifc.Operator):
style: bpy.props.IntProperty()
def _execute(self, context):
core.disable_editing_style(tool.Style) # So we don't end up soft-locking style edition
core.remove_style(tool.Ifc, tool.Style, style=tool.Ifc.get().by_id(self.style), reload_styles_ui=True)
@@ -40,7 +40,9 @@ class AssignType(bpy.types.Operator, tool.Ifc.Operator):
related_object: bpy.props.StringProperty()
def _execute(self, context):
type = tool.Ifc.get().by_id(self.relating_type or int(context.active_object.BIMTypeProperties.relating_type))
relating_type = tool.Ifc.get().by_id(
self.relating_type or int(context.active_object.BIMTypeProperties.relating_type)
)
related_objects = (
[bpy.data.objects.get(self.related_object)]
if self.related_object
@@ -49,9 +51,9 @@ class AssignType(bpy.types.Operator, tool.Ifc.Operator):
model_props = context.scene.BIMModelProperties
for obj in related_objects:
element = tool.Ifc.get_entity(obj)
core.assign_type(tool.Ifc, tool.Type, element=element, type=type)
core.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
if model_props.occurrence_name_style == "TYPE":
obj.name = tool.Model.generate_occurrence_name(type, element.is_a())
obj.name = tool.Model.generate_occurrence_name(relating_type, element.is_a())
class UnassignType(bpy.types.Operator, tool.Ifc.Operator):
+10 -8
View File
@@ -237,7 +237,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
activate_workspace: BoolProperty(
name="Activate BIM Workspace on Startup",
default=True,
description="If enabled, this will automatically activate the BIM workspace when opening a project.\It is recommended to keep this `Enabled`",
description="If enabled, this will automatically activate the BIM workspace when opening a project.\nIt is recommended to keep this `Enabled`",
)
should_setup_toolbar: BoolProperty(
name="Always Show Toolbar In 3D Viewport",
@@ -246,9 +246,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
)
should_play_chaching_sound: BoolProperty(name="Play A Cha-Ching Sound When Project Costs Updates", default=False)
spatial_elements_unselectable: BoolProperty(
name="Make Spatial Elements Unselectable By Default",
default=True,
description="If disabled, it will be possible to select spatial elements in the 3D viewport.\nIt is recommended to keep this `Enabled`, as this can have unintended consequences")
name="Make Spatial Elements Unselectable By Default",
default=True,
description="If disabled, it will be possible to select spatial elements in the 3D viewport.\nIt is recommended to keep this `Enabled`, as this can have unintended consequences",
)
decorations_colour: bpy.props.FloatVectorProperty(
name="Decorations Color", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
)
@@ -1191,15 +1192,16 @@ def draw_custom_context_menu(self: bpy.types.Menu, context: bpy.types.Context) -
url_op = layout.operator("bim.open_uri", icon="URL", text="Online IFC Documentation")
url_op.uri = url
class BIM_PT_decorators_overlay(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'HEADER'
bl_parent_id = 'VIEW3D_PT_overlay'
bl_space_type = "VIEW_3D"
bl_region_type = "HEADER"
bl_parent_id = "VIEW3D_PT_overlay"
bl_label = "Bonsai Decorators"
@classmethod
def poll(cls, context):
return context.mode == 'OBJECT'
return context.mode == "OBJECT"
def draw(self, context):
layout = self.layout
+6 -1
View File
@@ -86,12 +86,17 @@ def add_part_to_object(
assign_object(ifc, aggregator, collector, relating_obj=obj, related_obj=part_obj)
blender.set_active_object(obj)
def enable_aggregate_mode(aggregator: tool.Aggregate, obj: bpy.types.Object,):
def enable_aggregate_mode(
aggregator: tool.Aggregate,
obj: bpy.types.Object,
):
if aggregator.get_aggregate_mode():
disable_aggregate_mode(aggregator)
aggregator.enable_aggregate_mode(obj)
def disable_aggregate_mode(aggregator: tool.Aggregate):
aggregator.disable_aggregate_mode()
+1 -1
View File
@@ -425,7 +425,7 @@ def add_annotation(
ifc_representation_class=drawing_tool.get_ifc_representation_class(object_type),
)
ifc.run("group.assign_group", group=drawing_tool.get_drawing_group(drawing), products=[element])
collector.assign(obj)
collector.assign(obj, should_clean_users_collection=True)
drawing_tool.enable_editing(obj)
+4 -6
View File
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
def copy_class(
ifc: tool.Ifc, collector: tool.Collector, geometry: tool.Geometry, root: tool.Root, obj: bpy.types.Object
) -> ifcopenshell.entity_instance:
) -> ifcopenshell.entity_instance | None:
element = ifc.get_entity(obj)
if not element:
return
@@ -43,14 +43,12 @@ def copy_class(
ifc.run("type.map_type_representations", related_object=new, relating_type=relating_type)
root.link_object_data(ifc.get_object(relating_type), obj)
elif representation:
root.copy_representation(element, new)
new_representation = root.get_element_representation(new, root.get_representation_context(representation))
copied_entities = root.copy_representation(element, new)
data = geometry.duplicate_object_data(obj)
if data:
geometry.copy_data_links(data, copied_entities)
geometry.change_object_data(obj, data, is_global=True)
geometry.rename_object(data, geometry.get_representation_name(new_representation))
geometry.link(new_representation, data)
geometry.reload_representation_item_ids(new_representation, data)
geometry.rename_object(data, geometry.get_representation_name(ifc.get_entity(data)))
root.assign_body_styles(new, obj)
collector.assign(obj)
if root.is_element_a(new, "IfcOpeningElement"):
+2
View File
@@ -65,6 +65,8 @@ def remove_style(
avoid unnecessary reloads.
"""
obj = ifc.get_object(style)
if style_tool.is_editing_style() and obj == style_tool.get_currently_edited_material():
style_tool.disable_editing()
# Get style_type before removing object as later StylesData might fail to load
# due object not yet removed completely.
style_type = style_tool.get_active_style_type()
+2 -1
View File
@@ -387,6 +387,7 @@ class Geometry:
def clear_cache(cls, element): pass
def clear_modifiers(cls, obj): pass
def clear_scale(cls, obj): pass
def copy_data_links(cls, data, copied_entities) -> None: pass
def delete_data(cls, data): pass
def delete_ifc_object(cls, obj): pass
def delete_opening_object_placement(cls, opening): pass
@@ -420,7 +421,6 @@ class Geometry:
def record_object_position(cls, obj): pass
def recreate_object_with_data(cls, obj, data): pass
def reimport_element_representations(cls, obj, representation, apply_openings=True): pass
def reload_representation_item_ids(cls, representation, data) -> None: pass
def remove_connection(cls, connection): pass
def rename_object(cls, obj, name): pass
def replace_object_data_globally(cls, old_data, new_data): pass
@@ -1013,6 +1013,7 @@ class Style:
def get_uv_maps(cls, representation): pass
def import_presentation_styles(cls, style_type): pass
def import_surface_attributes(cls, style): pass
def is_editing_style(cls): pass
def is_editing_styles(cls): pass
def reload_material_from_ifc(cls, obj): pass
def is_style_side_attribute_edited(cls, style, new_attributes): pass
+1 -5
View File
@@ -144,7 +144,7 @@ class Blender(bonsai.core.tool.Blender):
@classmethod
def get_active_object(cls) -> bpy.types.Object:
return bpy.context.view_layer.objects.active
return getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active
@classmethod
def get_selected_objects(cls) -> set[bpy.types.Object]:
@@ -185,10 +185,6 @@ class Blender(bonsai.core.tool.Blender):
return context.scene.BIMMaterialProperties.materials[
context.scene.BIMMaterialProperties.active_material_index
].ifc_definition_id
elif obj_type == "MaterialSet":
return ifcopenshell.util.element.get_material(
tool.Ifc.get_entity(bpy.data.objects.get(obj)), should_skip_usage=True
).id()
elif obj_type == "MaterialSetItem":
return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id
elif obj_type == "Task":
+4 -3
View File
@@ -31,11 +31,11 @@ class Collector(bonsai.core.tool.Collector):
for users_collection in obj.users_collection:
if obj.BIMObjectProperties.collection == users_collection:
continue
# Users are free to user extra collections for their own
# Users are free to use extra collections for their own
# purposes except for the reserved keyword "Ifc" and
# "Collection" (which is the default collection that comes with
# a Blender session)
if "Ifc" in users_collection.name or users_collection.name == "Collection":
# a Blender session) and "Unsorted" (our special collection).
if "Ifc" in users_collection.name or users_collection.name in ("Collection", "Unsorted"):
users_collection.objects.unlink(obj)
element = tool.Ifc.get_entity(obj)
@@ -43,6 +43,7 @@ class Collector(bonsai.core.tool.Collector):
# Note that tool.Geometry.is_locked is only checked within the if
# statements for efficiency as it is a slow check.
tool.Geometry.lock_scale(obj)
if element.is_a("IfcGridAxis"):
if tool.Geometry.is_locked(element):
+19 -13
View File
@@ -132,7 +132,6 @@ class Geometry(bonsai.core.tool.Geometry):
obj.lock_rotation = (True, True, True)
obj.lock_rotation_w = True
obj.lock_rotations_4d = True
obj.lock_scale = (True, True, True)
@classmethod
def unlock_object(cls, obj: bpy.types.Object) -> None:
@@ -140,7 +139,6 @@ class Geometry(bonsai.core.tool.Geometry):
obj.lock_rotation = (False, False, False)
obj.lock_rotation_w = False
obj.lock_rotations_4d = False
obj.lock_scale = (False, False, False)
@classmethod
def lock_scale(cls, obj: bpy.types.Object) -> None:
@@ -259,10 +257,14 @@ class Geometry(bonsai.core.tool.Geometry):
# a cylinder) so dissolving edges should not be allowed.
mesh_element = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if (
mesh_element.is_a("IfcShapeRepresentation")
and ifcopenshell.util.representation.resolve_representation(mesh_element).RepresentationType
== "AdvancedBrep"
) or mesh_element.is_a("IfcAdvancedBrep") or not obj.data:
(
mesh_element.is_a("IfcShapeRepresentation")
and ifcopenshell.util.representation.resolve_representation(mesh_element).RepresentationType
== "AdvancedBrep"
)
or mesh_element.is_a("IfcAdvancedBrep")
or not obj.data
):
return
if hasattr(obj.data, "attributes") and (ios_edges_attribute := obj.data.attributes.get("ios_edges")):
# Edges from a forced triangulation are stored as True in a boolean attribute on the mesh
@@ -612,7 +614,7 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def get_representation_name(cls, representation: ifcopenshell.entity_instance) -> str:
return tool.Loader.get_mesh_name(representation.ContextOfItems.id(), representation.id())
return tool.Loader.get_mesh_name(representation)
@classmethod
def get_styles(
@@ -719,7 +721,7 @@ class Geometry(bonsai.core.tool.Geometry):
if not tool.Loader.is_native_swept_disk_solid(element, representation):
continue
if curve is None:
mesh_name = tool.Loader.get_mesh_name(context.id(), representation.id())
mesh_name = tool.Loader.get_mesh_name(representation)
native_data = {
"representation": representation,
# TODO: calculate mapped item matrix.
@@ -1729,8 +1731,14 @@ class Geometry(bonsai.core.tool.Geometry):
return results
@classmethod
def reload_representation_item_ids(cls, representation: ifcopenshell.entity_instance, data: bpy.types.Mesh) -> None:
data["ios_item_ids"] = [i["item"].id() for i in ifcopenshell.util.representation.resolve_items(representation)]
def copy_data_links(cls, data: bpy.types.Mesh, copied_entities: dict[int, ifcopenshell.entity_instance]) -> None:
representation = tool.Ifc.get_entity(data)
representation = copied_entities.get(representation.id(), representation)
tool.Ifc.link(representation, data)
if item_ids := data.get("ios_item_ids"):
data["ios_item_ids"] = [copied_entities.get(i, tool.Ifc.get().by_id(i)).id() for i in item_ids]
if item_ids := data.get("ios_edges_item_ids"):
data["ios_edges_item_ids"] = [copied_entities.get(i, tool.Ifc.get().by_id(i)).id() for i in item_ids]
@classmethod
def export_mesh_to_tessellation(
@@ -1795,9 +1803,7 @@ class Geometry(bonsai.core.tool.Geometry):
return False
@classmethod
def get_bvh_tree(cls, obj:bpy.types.Object) -> BVHTree:
def get_bvh_tree(cls, obj: bpy.types.Object) -> BVHTree:
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
bm.transform(obj.matrix_world)
return BVHTree.FromBMesh(bm)
+3 -2
View File
@@ -34,7 +34,6 @@ class Ifc(bonsai.core.tool.Ifc):
OBJECT_TYPE = Literal[
"Object",
"Material",
"MaterialSet",
"MaterialSetItem",
"Task",
"Cost",
@@ -102,7 +101,7 @@ class Ifc(bonsai.core.tool.Ifc):
Return None if object is not linked to IFC or it's linked to non-existent element.
"""
ifc = IfcStore.get_file()
if not ifc:
if not ifc or not obj:
return
props = None
@@ -110,6 +109,8 @@ class Ifc(bonsai.core.tool.Ifc):
props = obj.BIMObjectProperties
elif isinstance(obj, bpy.types.Material):
props = obj.BIMStyleProperties
else:
props = obj.BIMMeshProperties
if props and (ifc_definition_id := props.ifc_definition_id):
try:
+5 -7
View File
@@ -84,13 +84,12 @@ class Loader(bonsai.core.tool.Loader):
@classmethod
def get_mesh_name_from_shape(cls, geometry: ifcopenshell.geom.ShapeType) -> str:
representation_id = cls.get_representation_id_from_shape(geometry)
representation = tool.Ifc.get().by_id(representation_id)
context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0
return cls.get_mesh_name(context_id, representation_id)
return cls.get_mesh_name(tool.Ifc.get().by_id(representation_id))
@classmethod
def get_mesh_name(cls, context_id: int, representation_id: int) -> str:
return "{}/{}".format(context_id, representation_id)
def get_mesh_name(cls, representation: ifcopenshell.entity_instance) -> str:
context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0
return "{}/{}".format(context_id, representation.id())
@classmethod
def get_name(cls, element: ifcopenshell.entity_instance) -> str:
@@ -983,8 +982,7 @@ class Loader(bonsai.core.tool.Loader):
tool.Loader.load_indexed_colour_map(rep, mesh)
ios_edges_values = [
(e.vertices[0], e.vertices[1]) in ios_edges
or (e.vertices[1], e.vertices[0]) in ios_edges
(e.vertices[0], e.vertices[1]) in ios_edges or (e.vertices[1], e.vertices[0]) in ios_edges
for e in mesh.edges
]
tool.Blender.Attribute.fill_attribute(mesh, "ios_edges", "EDGE", "BOOLEAN", ios_edges_values)
+50 -1
View File
@@ -131,6 +131,12 @@ class Model(bonsai.core.tool.Model):
points.append(cls.convert_si_to_unit(list(local_point)))
return tool.Ifc.get().createIfcCartesianPointList2D(points)
@classmethod
def export_annotation_fill_area(cls, obj: bpy.types.Object) -> ifcopenshell.entity_instance | None:
result = cls.auto_detect_annotation_fill_area(obj, obj.data)
if isinstance(result, dict) and result["annotation_fill_area"]:
return tool.Ifc.get().add(result["annotation_fill_area"])
@classmethod
def export_profile(
cls, obj: bpy.types.Object, position: Optional[Matrix] = None
@@ -268,6 +274,12 @@ class Model(bonsai.core.tool.Model):
return obj
@classmethod
def import_annotation_fill_area(
cls, annotation_fill_area: ifcopenshell.entity_instance, obj: Optional[bpy.types.Object] = None
) -> bpy.types.Object:
return cls.import_profile(annotation_fill_area, obj)
@classmethod
def import_profile(
cls,
@@ -299,6 +311,10 @@ class Model(bonsai.core.tool.Model):
cls.convert_curve_to_mesh(obj, position, inner_curve)
elif profile.is_a() == "IfcRectangleProfileDef":
cls.import_rectangle(obj, position, profile)
elif profile.is_a() == "IfcAnnotationFillArea":
cls.convert_curve_to_mesh(obj, position, profile.OuterBoundary)
for inner_boundary in profile.InnerBoundaries or []:
cls.convert_curve_to_mesh(obj, position, inner_boundary)
mesh = bpy.data.meshes.new("Profile")
mesh.from_pydata(cls.vertices, cls.edges, [])
@@ -1247,8 +1263,12 @@ class Model(bonsai.core.tool.Model):
if stair_type == "WOOD/STEEL":
builder = ShapeBuilder(None)
# full tread rectangle
get_tread_verts = partial(builder.get_rectangle_coords, position=V_(0, -(tread_depth - tread_rise)))
def get_tread_verts(*args, **kwargs):
fn = partial(builder.get_rectangle_coords, position=V_(0, -(tread_depth - tread_rise)))
return [Vector(x) for x in fn(*args, **kwargs)]
default_tread_verts = get_tread_verts(size=V_(tread_run + nosing_overlap, tread_depth))
default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise)
@@ -1575,6 +1595,20 @@ class Model(bonsai.core.tool.Model):
)
tool.Model.replace_object_ifc_representation(body, obj, representation)
@classmethod
def auto_detect_annotation_fill_area(cls, obj: bpy.types.Object, mesh: bpy.types.Mesh) -> dict | None:
result = cls.auto_detect_profiles(obj, mesh)
fill_area = None
if isinstance(result, dict) and (profile_def := result["profile_def"]):
if profile_def.is_a("IfcArbitraryClosedProfileDef"):
fill_area = result["ifc_file"].createIfcAnnotationFillArea(profile_def.OuterCurve)
elif profile_def.is_a("IfcArbitraryProfileDefWithVoids"):
fill_area = result["ifc_file"].createIfcAnnotationFillArea(
profile_def.OuterCurve, profile_def.InnerCurves
)
if fill_area:
return {"ifc_file": result["ifc_file"], "annotation_fill_area": fill_area}
@classmethod
def auto_detect_profiles(
cls, obj: bpy.types.Object, mesh: bpy.types.Mesh, position: Matrix | None = None
@@ -1979,3 +2013,18 @@ class Model(bonsai.core.tool.Model):
@classmethod
def get_booleaned_obj(cls, boolean_obj: bpy.types.Object) -> bpy.types.Object:
return boolean_obj.data.BIMMeshProperties.obj
@classmethod
def bm_sort_out_geom(
cls, geom_data: list[Union[bmesh.types.BMVert, bmesh.types.BMEdge, bmesh.types.BMFace]]
) -> dict[str, Any]:
geom_dict = {"verts": [], "edges": [], "faces": []}
for el in geom_data:
if isinstance(el, bmesh.types.BMVert):
geom_dict["verts"].append(el)
elif isinstance(el, bmesh.types.BMFace):
geom_dict["faces"].append(el)
else:
geom_dict["edges"].append(el)
return geom_dict
+2 -4
View File
@@ -33,10 +33,8 @@ class Patch(bonsai.core.tool.Patch):
@classmethod
def is_filepath_argument(cls, recipe: str, arg_name: str) -> bool:
# TODO: Temporary hack to identify filepath arguments.
# Should mark them as such in the patches documentation
# and process it later.
return recipe == "SplitByBuildingStorey" and arg_name == "output_dir"
# There is probably a more explicit way to do this
return "filepath" in arg_name
@classmethod
def does_patch_has_output(cls, recipe: str) -> bool:
-2
View File
@@ -45,8 +45,6 @@ class Pset(bonsai.core.tool.Pset):
return bpy.data.objects.get(obj).PsetProperties
elif obj_type == "Material":
return bpy.context.scene.MaterialPsetProperties
elif obj_type == "MaterialSet":
return bpy.data.objects.get(obj).MaterialSetPsetProperties
elif obj_type == "MaterialSetItem":
return bpy.data.objects.get(obj).MaterialSetItemPsetProperties
elif obj_type == "Task":
+1 -1
View File
@@ -206,7 +206,7 @@ class Raycast(bonsai.core.tool.Raycast):
bm.free()
snapping_points = []
sorted_points = sorted(points)
sorted_points = sorted(points, key=lambda x: x[0])
for p in sorted_points:
point = copy.deepcopy(p)
snapping_points.append(point[1])
+14 -5
View File
@@ -55,28 +55,38 @@ class Root(bonsai.core.tool.Root):
)
@classmethod
def copy_representation(cls, source: ifcopenshell.entity_instance, dest: ifcopenshell.entity_instance) -> None:
def copy_representation(
cls, source: ifcopenshell.entity_instance, dest: ifcopenshell.entity_instance
) -> dict[int, ifcopenshell.entity_instance]:
def exclude_callback(attribute):
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
copied_entities: dict[int, ifcopenshell.entity_instance] = {}
if dest.is_a("IfcProduct"):
if not source.Representation:
return
return copied_entities
dest.Representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(),
source.Representation,
exclude=["IfcGeometricRepresentationContext"],
exclude_callback=exclude_callback,
copied_entities=copied_entities,
)
elif dest.is_a("IfcTypeProduct"):
if not source.RepresentationMaps:
return
return copied_entities
dest.RepresentationMaps = [
ifcopenshell.util.element.copy_deep(
tool.Ifc.get(), m, exclude=["IfcGeometricRepresentationContext"], exclude_callback=exclude_callback
tool.Ifc.get(),
m,
exclude=["IfcGeometricRepresentationContext"],
exclude_callback=exclude_callback,
copied_entities=copied_entities,
)
for m in source.RepresentationMaps
]
return copied_entities
@classmethod
def does_type_have_representations(cls, element: ifcopenshell.entity_instance) -> bool:
@@ -354,7 +364,6 @@ class Root(bonsai.core.tool.Root):
tool.Aggregate.constrain_all_parts_to_aggregate(tool.Ifc.get_object(new_aggregate[0]))
tool.Blender.select_and_activate_single_object(bpy.context, tool.Ifc.get_object(new_aggregate[0]))
@classmethod
def run_geometry_add_representation(
cls,
+4
View File
@@ -550,6 +550,10 @@ class Style(bonsai.core.tool.Style):
def is_editing_styles(cls) -> bool:
return bpy.context.scene.BIMStylesProperties.is_editing
@classmethod
def is_editing_style(cls) -> bool:
return bpy.context.scene.BIMStylesProperties.is_editing_style
@classmethod
def select_elements(cls, elements: list[ifcopenshell.entity_instance]) -> None:
for element in elements:
@@ -0,0 +1,110 @@
Debugging Bonsai
================
This is a mini-guide to setting up an IDE and configuring it to debug Bonsai more
easily. It is currently specific to this writers own system (Ubuntu) but this
can be expanded by others.
1. **Install VSCodium**: This will be system specific. I used the available
snap package.
.. code-block:: bash
sudo snap install --classic codium
I chose VSCodium to avoid Microsoft telemetry and data harvesting, but
VSCode is going to be similar, and even a bit easier (i.e. steps 3 & 4).
2. **Activate Python language support**: Start VSCodium, open the Extensions, find the Python
language support and activate it.
3. **Install Blender Addon**: It seems VSCodium is not allowed to directly access
the Marketplace, and this addon (and the next) is not available in
VSCodium's equivalent. (Or I'm an idiot... entirely possible). This means a
few more steps are needed.
Download the Blender addon's ``.vsix`` file from marketplace (Under "Resources")
using your browser from `this <https://marketplace.visualstudio.com/items?itemName=JacquesLucke.blender-development>`_
page.
Install it with "**...**" -> "**Install from VSIX...**"
4. **Install ms-vscode.cpptools**: When trying to use the Blender addon I got
an error about a missing dependancy. I read somewhere that this addon was
not needed, but I can't currently find the reference. At least for me, it
needs to be added the same way as step 3, or the Blender addon fails to
start.
Download the Cpp Tools addon's ``.vsix`` file from marketplace (Under "Resources")
using your browser from `this <https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools>`_
page. You will need to select the correct download for your system OS and
architecture.
Install it with "**...**" -> "**Install from VSIX...**"
5. **Set the Blender config directory**: In **Settings** -> **Extensions** -> **Blender** -> "**Environment Variables**"
edit the json file to set the ``BLENDER_USER_RESOURCE`` to the config folder
you installed Bonsai to. For me:
.. code-block:: json
{
"blender.environmentVariables": {
"BLENDER_USER_RESOURCES": "/home/steve/.config/blender/4.2bonsai"
}
}
.. note::
You `must` change that path to the correct value for your system.
6. **Unset the Just My Code flag**: In **Settings** -> **Extensions** -> **Blender** -> "**Just My Code**"
by unchecking the box.
Again, this was advice found searching around in a github issue. If not
done, the breakpoints do not work for me.
7. **Open the folder in VSCodium**: This is the same folder set in step 5.
``/home/steve/.config/blender/4.2bonsai``
This was a key point for me. Do `not` go deeper in the folder structure -
that just doesn't work. (Thanks @theoryshaw)
8. **Start Blender**: In VSCodium press ``Ctrl+Shift+P``, and search/select
**Blender: Start**.
9. **Set the Blender binary path**: The first time you try to start Blender the
addon will ask for the path of the binary. Navigate to your Blender binary
and select it. For me:
``/home/steve/Software/blender-git/build_linux/bin/blender``
.. warning::
Do `not` try to use a binary installed using snap - it will `not` work.
Either install a debian package of Blender, or build it from source, and
use that.
All being well, Blender should start and show the usual Bonsai UI.
10. **Ensure Blender is behaving**: Exercise the interface a bit to be sure
Bonsai is working normally. Add a demo project, add some walls, etc.
One issue I ran in to was opening the wrong folder in step 7. Due to the
way VSCodium works, I was getting two Bonsai plugins conflicting, and
failing in interesting ways. One symptom of this was the modal user
interface when adding walls was completely missing. Oh, and the debugger
completely failed to work.
If you are happy, quit Blender, and try setting some breakpoints in
VSCodium then run Blender again, and try to trigger them. I set one on
the ``bim.add_sheet`` operator at:
``extensions/.local/lib/python3.11/site-packages/bonsai/bim/module/drawing/operator.py:1456``
.. note::
I made the mistake of setting the breakpoint on the ``_execute``
methods ``def`` line, which did not work. This probably helped confuse
me when trying to get the addon to work.
If you get to this point, congratulations! You will now be 1000% more effective
when troubleshooting issues, and able to make many more contributions, fixes
and patches.
@@ -15,3 +15,4 @@ This chapter covers how you can help contribute to Bonsai.
translations
undo_system
writing_docs
debugging
+1
View File
@@ -58,6 +58,7 @@ and data-rich OpenBIM with Blender :)
guides/development/index
guides/authoring/other_addons
guides/troubleshooting
guides/debugging
.. toctree::
:hidden: