mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-06 20:36:27 +00:00
Merge branch 'v0.8.0' of https://github.com/IfcOpenShell/IfcOpenShell into update_type_manager
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -58,6 +58,7 @@ and data-rich OpenBIM with Blender :)
|
||||
guides/development/index
|
||||
guides/authoring/other_addons
|
||||
guides/troubleshooting
|
||||
guides/debugging
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
||||
Reference in New Issue
Block a user