diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index 2be31b25c1..74dd0a2e7f 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -95,6 +95,7 @@ classes = ( profile.ChangeCardinalPoint, profile.ChangeProfileDepth, profile.DisableEditingExtrusionAxis, + profile.DrawPolylineProfile, profile.EditExtrusionAxis, profile.EnableEditingExtrusionAxis, profile.ExtendProfile, diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 86c5e72e55..b426f42736 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -25,7 +25,7 @@ import bmesh import ifcopenshell import bonsai.tool as tool import math -from math import sin, cos, tan, radians +from math import sin, cos, tan, radians, atan2 from bpy.types import SpaceView3D from bpy_extras import view3d_utils from mathutils import Vector, Matrix, Quaternion @@ -981,22 +981,24 @@ class ProductDecorator: def get_profile_preview_data(self, context, relating_type): material = ifcopenshell.util.element.get_material(relating_type) try: - profile = material.MaterialProfiles[0].Profile + profile_curve = 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 + + 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)] settings = ifcopenshell.geom.settings() settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) - shape = ifcopenshell.geom.create_shape(settings, profile) + shape = ifcopenshell.geom.create_shape(settings, profile_curve) verts = shape.verts if not verts: @@ -1036,78 +1038,65 @@ class ProductDecorator: case "9": grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts] - # Create extrusion bmesh + + 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) - 1) + + for i, point in enumerate(profile_curve.splines[0].points): + point.co = Vector((*grouped_verts[i], 0)) + + profile_obj = bpy.data.objects.new("Profile", profile_curve) + + preview_curve = bpy.data.curves.new("Polyline", type='CURVE') + preview_curve.dimensions = "2D" + preview_curve.splines.new('POLY') + preview_curve.splines[0].points.add(len(polyline_verts) - 1) + for i, point in enumerate(preview_curve.splines[0].points): + point.co = Vector((*polyline_verts[i], 0)) + preview_curve.splines[0].use_smooth = False + preview_curve.bevel_mode = "OBJECT" + preview_curve.bevel_object = profile_obj + + preview_obj = bpy.data.objects.new("Preview", preview_curve) + context.scene.collection.objects.link(preview_obj) + bpy.context.view_layer.objects.active = preview_obj + selection = preview_obj.select_get() + bpy.ops.object.select_all(action="DESELECT") + preview_obj.select_set(True) + bpy.ops.object.convert(target="MESH") + preview_obj = bpy.data.objects["Preview"] + 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)) - + new_verts = [bm.verts.new(v.co) for v in preview_obj.data.vertices] + index = [[v for v in edge.vertices] for edge in preview_obj.data.edges] + new_edges = [bm.edges.new((new_verts[i[0]], new_verts[i[1]])) for i in index] + for face in preview_obj.data.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()] - # 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)) 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["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 + bpy.data.objects.remove(bpy.data.objects[preview_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(preview_obj.data, do_unlink=True) + except: + pass + bm.free() + return data def draw_product_preview(self, context): @@ -1152,6 +1141,7 @@ class ProductDecorator: ) # Profile type products + self.line_shader.uniform_float("lineWidth", 0.5) product_preview_data = self.get_profile_preview_data(context, self.relating_type) if product_preview_data: self.draw_batch("LINES", product_preview_data["verts"], decorator_color, product_preview_data["edges"]) diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index e10eb578bf..442b04efab 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -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 @@ -86,6 +109,9 @@ class DumbProfileGenerator: "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,115 @@ 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) + + 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"} + diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 7e56054858..d02804ff6c 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -991,6 +991,10 @@ 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" + ): + bpy.ops.bim.draw_polyline_profile("INVOKE_DEFAULT") else: bpy.ops.bim.add_occurrence("INVOKE_DEFAULT")