From e55955b72a6f7d33ca7cbd7ca3deea4f474de8cd Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 21 Jan 2026 18:11:20 +1100 Subject: [PATCH] See #7566. Fix unusable lag due to dense meshes with product preview Previously, when the add occurrence modal operator was executed, on every modal loop (i.e. every mouse movement) it would fetch the mesh geometry to be previewed, store the verts / edges / faces in mesh collections, then the decorator would fetch that geometry, the clear the collections, in a loop. I've removed the Blender collections. Instead the same strategy is used as in ItemDecorator i.e. the mesh and verts are fetched once during decorator installation, then on each draw call only a single vertex loop to multiply by the transformation matrix for snapping and mouse position. You can test with the LOD400 model in #7566. On my machine it would cause lag on anything with >500 faces. Now it seems to work without lag on a 26k polygon mesh. --- .../bonsai/bim/module/geometry/decorator.py | 23 +- .../bonsai/bim/module/model/__init__.py | 3 - .../bonsai/bim/module/model/decorator.py | 593 +++++++++++++++++- .../bonsai/bim/module/model/polyline.py | 586 +---------------- src/bonsai/bonsai/bim/module/model/product.py | 2 - src/bonsai/bonsai/bim/module/model/profile.py | 3 - src/bonsai/bonsai/bim/module/model/prop.py | 11 - src/bonsai/bonsai/bim/module/model/slab.py | 3 - src/bonsai/bonsai/bim/module/model/wall.py | 3 - src/bonsai/bonsai/tool/model.py | 23 +- 10 files changed, 594 insertions(+), 656 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/decorator.py b/src/bonsai/bonsai/bim/module/geometry/decorator.py index 0888dd66c4..02cb4bd255 100644 --- a/src/bonsai/bonsai/bim/module/geometry/decorator.py +++ b/src/bonsai/bonsai/bim/module/geometry/decorator.py @@ -75,27 +75,8 @@ class ItemDecorator: special_edges = [] if (total_triangles := len(obj.data.loop_triangles)) > 0: - # TODO: this is a far too small threshold. - # This is just a stopgap optimisation until other slowdowns are solved. - if total_triangles > 500: - vert_map = {} - i = 0 - verts = [] - tris = [] - for tri in obj.data.loop_triangles[:500]: - new_tri = [] - for vert in tri.vertices: - if vert in vert_map: - new_tri.append(vert_map[vert]) - else: - vert_map[vert] = i - new_tri.append(i) - i += 1 - verts.append(tuple(obj.matrix_world @ obj.data.vertices[vert].co)) - tris.append(new_tri) - else: - verts = [tuple(obj.matrix_world @ v.co) for v in obj.data.vertices] - tris = [tuple(t.vertices) for t in obj.data.loop_triangles] + verts = [tuple(obj.matrix_world @ v.co) for v in obj.data.vertices] + tris = [tuple(t.vertices) for t in obj.data.loop_triangles] i = len(verts) matrix_world = obj.matrix_world diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index 47ef5a0343..c758aa7f54 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -140,7 +140,6 @@ classes = ( prop.BIMRailingProperties, prop.BIMRoofProperties, prop.BIMPolylineProperties, - prop.BIMProductPreviewProperties, prop.BIMExternalParametricGeometryProperties, ui.BIM_PT_array, ui.BIM_PT_stair, @@ -263,7 +262,6 @@ 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) @@ -288,7 +286,6 @@ 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 diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index aad4c647ee..43d770828e 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -26,10 +26,10 @@ import ifcopenshell import bonsai.tool as tool import math import mathutils -from math import sin, cos, radians +from math import sin, cos, tan, radians from bpy.types import SpaceView3D from bpy_extras import view3d_utils -from mathutils import Vector, Matrix +from mathutils import Vector, Matrix, Quaternion from gpu_extras.batch import batch_for_shader from gpu_extras.presets import draw_circle_2d from typing import Union @@ -879,13 +879,43 @@ class PolylineDecorator: class ProductDecorator: is_installed = False handlers = [] + preview_mode: Literal["PROFILE_VERTICAL", "PROFILE_HORIZONTAL", "LAYER2", "LAYER3", "GENERIC"] relating_type = None + obj_data: dict[str, list] = {} + obj_matrix_i = None @classmethod def install(cls, context): + from bonsai.bim.module.geometry.decorator import ItemDecorator + if cls.is_installed: cls.uninstall() + + props = tool.Model.get_model_props() + handler = cls() + if ( + (props.relating_type_id) + and (relating_type := tool.Ifc.get().by_id(int(props.relating_type_id))) + and (relating_type_obj := tool.Ifc.get_object(relating_type)) + ): + handler.relating_type = relating_type + if tool.Model.get_usage_type(relating_type) == "PROFILE": + if relating_type.is_a() in {"IfcColumnType", "IfcPileType"}: + handler.preview_mode = "PROFILE_VERTICAL" + else: + handler.preview_mode = "PROFILE_HORIZONTAL" + elif tool.Model.get_usage_type(relating_type) == "LAYER2": + handler.preview_mode = "LAYER2" + elif tool.Model.get_usage_type(relating_type) == "LAYER3": + handler.preview_mode = "LAYER3" + else: + handler.preview_mode = "GENERIC" + if relating_type_obj.data: + handler.obj_data = ItemDecorator.get_obj_data(relating_type_obj) + handler.obj_data["raw_verts"] = [Vector(v) for v in handler.obj_data["verts"]] + handler.obj_matrix_i = relating_type_obj.matrix_world.inverted() + cls.handlers.append( SpaceView3D.draw_handler_add(handler.draw_product_preview, (context,), "WINDOW", "POST_VIEW") ) @@ -893,10 +923,6 @@ class ProductDecorator: @classmethod def uninstall(cls): - props = tool.Model.get_product_preview_props() # 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") @@ -912,14 +938,6 @@ class ProductDecorator: shader.uniform_float("color", color) batch.draw(shader) - def get_product_preview_data(self, context) -> dict[str, Any]: - props = tool.Model.get_product_preview_props() - data: dict[str, Any] = {} - 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): def transparent_color(color, alpha=0.1): color = [i for i in color] @@ -945,12 +963,549 @@ class ProductDecorator: else: return - 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( - "TRIS", product_preview_data["verts"], transparent_color(decorator_color), product_preview_data["tris"] + if self.preview_mode == "LAYER2": + data = self.get_wall_preview_data() + elif self.preview_mode == "LAYER3": + data = self.get_slab_preview_data() + elif self.preview_mode == "PROFILE_VERTICAL": + data = self.get_vertical_profile_preview_data() + elif self.preview_mode == "PROFILE_HORIZONTAL": + data = self.get_horizontal_profile_preview_data() + elif self.preview_mode == "GENERIC": + data = self.get_generic_preview_data() + if data: + self.draw_batch("LINES", data["verts"], decorator_color, data["edges"]) + self.draw_batch("TRIS", data["verts"], transparent_color(decorator_color), data["tris"]) + + def get_wall_preview_data(self): + relating_type = self.relating_type + # Get properties from object type + model_props = tool.Model.get_model_props() + direction_sense = model_props.direction_sense + direction = 1 + if direction_sense == "NEGATIVE": + direction = -1 + + layers = tool.Model.get_material_layer_parameters(relating_type) + if not layers["thickness"]: + return + thickness = layers["thickness"] + thickness *= direction + + offset_type = model_props.offset_type_vertical + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + offset = model_props.offset * unit_scale + + height = float(model_props.extrusion_depth) + rl = float(model_props.rl1) + x_angle = float(model_props.x_angle) + if x_angle > radians(90) or x_angle < radians(-90): + height *= -1 + angle_distance = height * tan(x_angle) + thickness *= 1 / cos(x_angle) + + data = {} + data["verts"] = [] + + # Verts + polyline_vertices = [] + polyline_props = tool.Model.get_polyline_props() + polyline_data = polyline_props.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 = tool.Model.create_bmesh_from_vertices(polyline_vertices, is_closed) + 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_distance + offset) + offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distance + 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_slab_preview_data(self): + relating_type = self.relating_type + model_props = tool.Model.get_model_props() + x_angle = 0 if tool.Cad.is_x(model_props.x_angle, 0, tolerance=0.001) else model_props.x_angle + direction_sense = model_props.direction_sense + direction = 1 + if direction_sense == "NEGATIVE": + direction = -1 + + layers = tool.Model.get_material_layer_parameters(relating_type) + if not layers["thickness"]: + return + thickness = layers["thickness"] * abs(1 / cos(x_angle)) + thickness *= direction + + offset_type = model_props.offset_type_horizontal + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + offset = model_props.offset * abs(1 / cos(x_angle)) * unit_scale + + data = {} + data["verts"] = [] + # Verts + polyline_vertices = [] + polyline_props = tool.Model.get_polyline_props() + polyline_data = polyline_props.insertion_polyline + polyline_points = polyline_data[0].polyline_points if polyline_data else [] + if len(polyline_points) < 3: + data = [] + return + for point in polyline_points: + polyline_vertices.append(Vector((point.x, point.y, point.z))) + if x_angle: + # Get vertices relative to the first polyline point as origin + local_vertices = [v - Vector(polyline_vertices[0]) for v in polyline_vertices] + # Make the transformation relative to the x_angle + transformed_vertices = [Vector((v.x, v.y * (1 / cos(x_angle)), v.z)) for v in local_vertices] + # Convert back to world origin + polyline_vertices = [v + Vector(polyline_vertices[0]) for v in transformed_vertices] + if offset != 0: + polyline_vertices = [v + Vector((0, 0, offset)) for v in polyline_vertices] + is_closed = True + 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 + ): + polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed. + bm = tool.Model.create_bmesh_from_vertices(polyline_vertices, is_closed) + bm.verts.ensure_lookup_table() + if x_angle: + rot_mat = Matrix.Rotation(x_angle, 3, "X") + if abs(x_angle) > (pi / 2): + rot_mat = rot_mat @ Matrix.Scale(-1, 3, (0, 1, 0)) + bmesh.ops.rotate(bm, cent=Vector(bm.verts[0].co), verts=bm.verts, matrix=rot_mat) + new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges) + new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + 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, thickness)) + bm.verts.index_update() + bm.edges.index_update() + verts = [tuple(v.co) for v in bm.verts] + edges = [[v.index for v in e.verts] for e in bm.edges] + tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()] + data["verts"] = verts + data["edges"] = edges + data["tris"] = tris + return data + + def get_vertical_profile_preview_data(self) -> dict[str, Any]: + relating_type = self.relating_type + material = ifcopenshell.util.element.get_material(relating_type) + try: + profile = material.MaterialProfiles[0].Profile + except: + return {} + + model_props = tool.Model.get_model_props() + 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(f"Profile shape has no vertices, it probably is invalid: '{profile}'.") + + 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 + polyline_props = tool.Model.get_polyline_props() + snap_prop = polyline_props.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(self) -> dict[str, Any]: + relating_type = self.relating_type + material = ifcopenshell.util.element.get_material(relating_type) + try: + profile_curve = material.MaterialProfiles[0].Profile + except: + return {} + + model_props = tool.Model.get_model_props() + cardinal_point = model_props.cardinal_point + + polyline_verts = [] + polyline_props = tool.Model.get_polyline_props() + polyline_data = polyline_props.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(f"Profile shape has no vertices, it probably is invalid: '{profile_curve}'.") + + 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] + + data: dict[str, Any] = {} + data["verts"] = [] + data["edges"] = [] + data["tris"] = [] + + grouped_verts = [(v) for v in grouped_verts] + + all_bm = bmesh.new() + for i in range(len(polyline_verts) - 1): + mesh = bpy.data.meshes.new("TempMesh") + # Create the initial mesh from the profile verts + bm = tool.Model.create_bmesh_from_vertices(grouped_verts, is_closed=True) + bm.verts.ensure_lookup_table() + # Creates the clipping plane formed by two segments. + # The first one is for the profile start, based on the current and previous segment of the polyline. + # The second is for the profile end, based on the current and the next segment. + if i == 0: + d = (polyline_verts[i + 1] - polyline_verts[i]).normalized() + clip_start = d + else: + d1 = (polyline_verts[i] - polyline_verts[i - 1]).normalized() + d2 = (polyline_verts[i] - polyline_verts[i + 1]).normalized() + clip_start = (d1 - d2).normalized() + + if i == len(polyline_verts) - 2: + d = (polyline_verts[i + 1] - polyline_verts[i]).normalized() + clip_end = d + else: + d1 = (polyline_verts[i + 1] - polyline_verts[i]).normalized() + d2 = (polyline_verts[i + 1] - polyline_verts[i + 2]).normalized() + clip_end = (d1 - d2).normalized() + + # Rotates the profile face to the right direction + direction = polyline_verts[i + 1] - polyline_verts[i] + position = polyline_verts[i] + rotation_matrix = direction.to_track_quat("Z", "Y").to_matrix().to_4x4() + bmesh.ops.transform(bm, verts=bm.verts, matrix=rotation_matrix) + bmesh.ops.translate(bm, verts=bm.verts, vec=position) + bmesh.ops.translate(bm, verts=bm.verts, vec=-direction) + + # Extrude and move the new face + last_face = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:]) + new_verts = [e for e in last_face["geom"] if isinstance(e, bmesh.types.BMVert)] + bmesh.ops.translate(bm, verts=new_verts, vec=direction * 3) + # Apply the cutting planes + cut = bmesh.ops.bisect_plane( + bm, + geom=bm.verts[:] + bm.edges[:] + bm.faces[:], + plane_co=polyline_verts[i], + plane_no=clip_start, + clear_inner=True, ) + bm.verts.index_update() + bm.edges.index_update() + cut = bmesh.ops.bisect_plane( + bm, + geom=bm.verts[:] + bm.edges[:] + bm.faces[:], + plane_co=polyline_verts[i + 1], + plane_no=clip_end, + clear_outer=True, + ) + + bm.to_mesh(mesh) + bm.free() + mesh.update() + all_bm.from_mesh(mesh) + bpy.data.meshes.remove(bpy.data.meshes["TempMesh"]) + + # It's necessary to add the mesh to an object to get the expected result. + mesh = bpy.data.meshes.new("TempMesh2") + all_bm.to_mesh(mesh) + all_bm.free() + obj = bpy.data.objects.new("TempObj", mesh) + bm = bmesh.new() + bm.from_mesh(obj.data) + bpy.data.meshes.remove(bpy.data.meshes["TempMesh2"]) + + verts = [tuple(v.co) for v in bm.verts] + edges = [[v.index for v in e.verts] for e in bm.edges] + tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()] + data["verts"] = verts + data["edges"] = edges + data["tris"] = tris + bm.free() + return data + + def get_generic_preview_data(self): + if not (data := self.obj_data): + return + relating_type = self.relating_type + model_props = tool.Model.get_model_props() + 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 + polyline_props = tool.Model.get_polyline_props() + snap_prop = polyline_props.snap_mouse_point[0] + default_container_elevation = tool.Root.get_default_container_elevation() + 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 = Matrix() + if relating_type.is_a() in ["IfcDoorType", "IfcWindowType"] and snap_element and snap_element.is_a("IfcWall"): + layers = tool.Model.get_material_layer_parameters(snap_element) + axes = tool.Model.get_wall_axis(snap_obj, layers=layers) + axis_base = axes["base"] + axis_side = axes["side"] + point_on_base_axis = tool.Cad.point_on_edge(mouse_point, axis_base) + point_on_side_axis = tool.Cad.point_on_edge(mouse_point, axis_side) + if (point_on_base_axis - mouse_point).length_squared <= (point_on_side_axis - mouse_point).length_squared: + # mouse is snapped to the base axis, the preview looks exactly like the placed door / window + rot_mat = snap_obj.matrix_world + else: + # mouse is snapped to the side axis, the preview is inverted, rotate it now and correct x position later + rot_mat = ( + (snap_obj.matrix_world.to_quaternion() @ Quaternion(Vector((0, 0, 1)), radians(180))) + .to_matrix() + .to_4x4() + ) + + mouse_point.z = snap_obj.matrix_world.translation.z + + if snap_element and (container := ifcopenshell.util.element.get_container(snap_element)): + container_obj = tool.Ifc.get_object(container) + mouse_point.z = container_obj.location.z + + obj_type = tool.Ifc.get_object(relating_type) + + subcontexts = tool.Drawing.get_active_drawing_subcontexts() + if not subcontexts: + subcontexts = [("Model", "Body", "MODEL_VIEW")] + + active_context = tool.Geometry.get_active_representation_context(obj_type) + active_context_params = tool.Geometry.get_subcontext_parameters(active_context) + for subcontext in subcontexts: + if subcontext == active_context_params: + break + + representation = ifcopenshell.util.representation.get_representation(relating_type, *subcontext) + if representation: + bonsai.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj_type, + representation, + ) + context.view_layer.update() + break + + translate_mouse = Matrix.Translation(mouse_point) + translate_rl = Matrix.Translation((0.0, 0.0, rl)) + combined_m = translate_mouse @ rot_mat @ translate_rl @ self.obj_matrix_i + data["verts"] = [tuple(combined_m @ v) for v in data["raw_verts"]] + return data class WallAxisDecorator: diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index b389f2be1f..d2514321f3 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -35,565 +35,12 @@ import bonsai.core.root import bonsai.core.geometry import bonsai.core.model as core import bonsai.tool as tool -from math import pi, sin, cos, degrees, tan, radians -from mathutils import Vector, Matrix, Quaternion -from bonsai.bim.module.model.opening import FilledOpeningGenerator +from mathutils import Vector from bonsai.bim.module.model.decorator import PolylineDecorator -from bonsai.bim.module.geometry.decorator import ItemDecorator from typing import Optional, Union, Literal, Any from lark import Lark, Transformer -def create_bmesh_from_vertices(vertices, is_closed=False): - bm = bmesh.new() - - new_verts = [bm.verts.new(v) for v in 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 - - -def get_wall_preview_data(context, relating_type): - # Get properties from object type - model_props = tool.Model.get_model_props() - direction_sense = model_props.direction_sense - direction = 1 - if direction_sense == "NEGATIVE": - direction = -1 - - layers = tool.Model.get_material_layer_parameters(relating_type) - if not layers["thickness"]: - return - thickness = layers["thickness"] - thickness *= direction - - offset_type = model_props.offset_type_vertical - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - offset = model_props.offset * unit_scale - - height = float(model_props.extrusion_depth) - rl = float(model_props.rl1) - x_angle = float(model_props.x_angle) - if x_angle > radians(90) or x_angle < radians(-90): - height *= -1 - angle_distance = height * tan(x_angle) - thickness *= 1 / cos(x_angle) - - data = {} - data["verts"] = [] - - # Verts - polyline_vertices = [] - polyline_props = tool.Model.get_polyline_props() - polyline_data = polyline_props.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, is_closed) - 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_distance + offset) - offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distance + 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_slab_preview_data(context, relating_type): - model_props = tool.Model.get_model_props() - x_angle = 0 if tool.Cad.is_x(model_props.x_angle, 0, tolerance=0.001) else model_props.x_angle - direction_sense = model_props.direction_sense - direction = 1 - if direction_sense == "NEGATIVE": - direction = -1 - - layers = tool.Model.get_material_layer_parameters(relating_type) - if not layers["thickness"]: - return - thickness = layers["thickness"] * abs(1 / cos(x_angle)) - thickness *= direction - - offset_type = model_props.offset_type_horizontal - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - offset = model_props.offset * abs(1 / cos(x_angle)) * unit_scale - - data = {} - data["verts"] = [] - # Verts - polyline_vertices = [] - polyline_props = tool.Model.get_polyline_props() - polyline_data = polyline_props.insertion_polyline - polyline_points = polyline_data[0].polyline_points if polyline_data else [] - if len(polyline_points) < 3: - data = [] - return - for point in polyline_points: - polyline_vertices.append(Vector((point.x, point.y, point.z))) - if x_angle: - # Get vertices relative to the first polyline point as origin - local_vertices = [v - Vector(polyline_vertices[0]) for v in polyline_vertices] - # Make the transformation relative to the x_angle - transformed_vertices = [Vector((v.x, v.y * (1 / cos(x_angle)), v.z)) for v in local_vertices] - # Convert back to world origin - polyline_vertices = [v + Vector(polyline_vertices[0]) for v in transformed_vertices] - if offset != 0: - polyline_vertices = [v + Vector((0, 0, offset)) for v in polyline_vertices] - is_closed = True - 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 - ): - polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed. - bm = create_bmesh_from_vertices(polyline_vertices, is_closed) - bm.verts.ensure_lookup_table() - if x_angle: - rot_mat = Matrix.Rotation(x_angle, 3, "X") - if abs(x_angle) > (pi / 2): - rot_mat = rot_mat @ Matrix.Scale(-1, 3, (0, 1, 0)) - bmesh.ops.rotate(bm, cent=Vector(bm.verts[0].co), verts=bm.verts, matrix=rot_mat) - new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges) - new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + 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, thickness)) - bm.verts.index_update() - bm.edges.index_update() - verts = [tuple(v.co) for v in bm.verts] - edges = [[v.index for v in e.verts] for e in bm.edges] - tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()] - data["verts"] = verts - data["edges"] = edges - data["tris"] = tris - return data - - -def get_vertical_profile_preview_data( - context: bpy.types.Context, relating_type: ifcopenshell.entity_instance -) -> dict[str, Any]: - material = ifcopenshell.util.element.get_material(relating_type) - try: - profile = material.MaterialProfiles[0].Profile - except: - return {} - - model_props = tool.Model.get_model_props() - 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(f"Profile shape has no vertices, it probably is invalid: '{profile}'.") - - 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 - polyline_props = tool.Model.get_polyline_props() - snap_prop = polyline_props.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: bpy.types.Context, relating_type: ifcopenshell.entity_instance -) -> dict[str, Any]: - material = ifcopenshell.util.element.get_material(relating_type) - try: - profile_curve = material.MaterialProfiles[0].Profile - except: - return {} - - model_props = tool.Model.get_model_props() - cardinal_point = model_props.cardinal_point - - polyline_verts = [] - polyline_props = tool.Model.get_polyline_props() - polyline_data = polyline_props.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(f"Profile shape has no vertices, it probably is invalid: '{profile_curve}'.") - - 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] - - data: dict[str, Any] = {} - data["verts"] = [] - data["edges"] = [] - data["tris"] = [] - - grouped_verts = [(v) for v in grouped_verts] - - all_bm = bmesh.new() - for i in range(len(polyline_verts) - 1): - mesh = bpy.data.meshes.new("TempMesh") - # Create the initial mesh from the profile verts - bm = create_bmesh_from_vertices(grouped_verts, is_closed=True) - bm.verts.ensure_lookup_table() - # Creates the clipping plane formed by two segments. - # The first one is for the profile start, based on the current and previous segment of the polyline. - # The second is for the profile end, based on the current and the next segment. - if i == 0: - d = (polyline_verts[i + 1] - polyline_verts[i]).normalized() - clip_start = d - else: - d1 = (polyline_verts[i] - polyline_verts[i - 1]).normalized() - d2 = (polyline_verts[i] - polyline_verts[i + 1]).normalized() - clip_start = (d1 - d2).normalized() - - if i == len(polyline_verts) - 2: - d = (polyline_verts[i + 1] - polyline_verts[i]).normalized() - clip_end = d - else: - d1 = (polyline_verts[i + 1] - polyline_verts[i]).normalized() - d2 = (polyline_verts[i + 1] - polyline_verts[i + 2]).normalized() - clip_end = (d1 - d2).normalized() - - # Rotates the profile face to the right direction - direction = polyline_verts[i + 1] - polyline_verts[i] - position = polyline_verts[i] - rotation_matrix = direction.to_track_quat("Z", "Y").to_matrix().to_4x4() - bmesh.ops.transform(bm, verts=bm.verts, matrix=rotation_matrix) - bmesh.ops.translate(bm, verts=bm.verts, vec=position) - bmesh.ops.translate(bm, verts=bm.verts, vec=-direction) - - # Extrude and move the new face - last_face = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:]) - new_verts = [e for e in last_face["geom"] if isinstance(e, bmesh.types.BMVert)] - bmesh.ops.translate(bm, verts=new_verts, vec=direction * 3) - # Apply the cutting planes - cut = bmesh.ops.bisect_plane( - bm, - geom=bm.verts[:] + bm.edges[:] + bm.faces[:], - plane_co=polyline_verts[i], - plane_no=clip_start, - clear_inner=True, - ) - bm.verts.index_update() - bm.edges.index_update() - cut = bmesh.ops.bisect_plane( - bm, - geom=bm.verts[:] + bm.edges[:] + bm.faces[:], - plane_co=polyline_verts[i + 1], - plane_no=clip_end, - clear_outer=True, - ) - - bm.to_mesh(mesh) - bm.free() - mesh.update() - all_bm.from_mesh(mesh) - bpy.data.meshes.remove(bpy.data.meshes["TempMesh"]) - - # It's necessary to add the mesh to an object to get the expected result. - mesh = bpy.data.meshes.new("TempMesh2") - all_bm.to_mesh(mesh) - all_bm.free() - obj = bpy.data.objects.new("TempObj", mesh) - bm = bmesh.new() - bm.from_mesh(obj.data) - bpy.data.meshes.remove(bpy.data.meshes["TempMesh2"]) - - verts = [tuple(v.co) for v in bm.verts] - edges = [[v.index for v in e.verts] for e in bm.edges] - tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()] - data["verts"] = verts - data["edges"] = edges - data["tris"] = tris - bm.free() - return data - - -def get_generic_product_preview_data(context, relating_type): - model_props = tool.Model.get_model_props() - 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 - polyline_props = tool.Model.get_polyline_props() - snap_prop = polyline_props.snap_mouse_point[0] - default_container_elevation = tool.Root.get_default_container_elevation() - 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 = Matrix() - if relating_type.is_a() in ["IfcDoorType", "IfcWindowType"] and snap_element and snap_element.is_a("IfcWall"): - layers = tool.Model.get_material_layer_parameters(snap_element) - axes = tool.Model.get_wall_axis(snap_obj, layers=layers) - axis_base = axes["base"] - axis_side = axes["side"] - point_on_base_axis = tool.Cad.point_on_edge(mouse_point, axis_base) - point_on_side_axis = tool.Cad.point_on_edge(mouse_point, axis_side) - if (point_on_base_axis - mouse_point).length_squared <= (point_on_side_axis - mouse_point).length_squared: - # mouse is snapped to the base axis, the preview looks exactly like the placed door / window - rot_mat = snap_obj.matrix_world - else: - # mouse is snapped to the side axis, the preview is inverted, rotate it now and correct x position later - rot_mat = (snap_obj.matrix_world.to_quaternion() @ Quaternion(Vector((0, 0, 1)), radians(180))).to_matrix().to_4x4() - - mouse_point.z = snap_obj.matrix_world.translation.z - - if snap_element and (container := ifcopenshell.util.element.get_container(snap_element)): - container_obj = tool.Ifc.get_object(container) - mouse_point.z = container_obj.location.z - - obj_type = tool.Ifc.get_object(relating_type) - - subcontexts = tool.Drawing.get_active_drawing_subcontexts() - if not subcontexts: - subcontexts = [("Model", "Body", "MODEL_VIEW")] - - active_context = tool.Geometry.get_active_representation_context(obj_type) - active_context_params = tool.Geometry.get_subcontext_parameters(active_context) - for subcontext in subcontexts: - if subcontext == active_context_params: - break - - representation = ifcopenshell.util.representation.get_representation(relating_type, *subcontext) - if representation: - bonsai.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj_type, - representation, - ) - context.view_layer.update() - break - - if obj_type.data: - data = ItemDecorator.get_obj_data(obj_type) - obj_type_matrix_i = obj_type.matrix_world.inverted() - translate_mouse = Matrix.Translation(mouse_point) - translate_rl = Matrix.Translation((0.0, 0.0, rl)) - combined_m = translate_mouse @ rot_mat @ translate_rl @ obj_type_matrix_i - data["verts"] = [tuple(combined_m @ Vector(v)) for v in data["verts"]] - return data - - class PolylineOperator: # TODO Fill doc strings """ """ @@ -989,37 +436,6 @@ class PolylineOperator: tool.Blender.update_viewport() return {"RUNNING_MODAL"} - def get_product_preview_data(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_instance) -> None: - if tool.Model.get_usage_type(relating_type) == "PROFILE": - if relating_type.is_a() in {"IfcColumnType", "IfcPileType"}: - data = get_vertical_profile_preview_data(context, relating_type) - else: - data = get_horizontal_profile_preview_data(context, relating_type) - elif tool.Model.get_usage_type(relating_type) == "LAYER2": - data = get_wall_preview_data(context, relating_type) - elif tool.Model.get_usage_type(relating_type) == "LAYER3": - data = get_slab_preview_data(context, relating_type) - else: - data = get_generic_product_preview_data(context, relating_type) - - # Update properties so it can be used by the decorator - props = tool.Model.get_product_preview_props() - props.verts.clear() - props.edges.clear() - props.tris.clear() - if not data: - return - - 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 set_offset(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_instance) -> None: props = tool.Model.get_model_props() direction_sense = props.direction_sense diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index c08ff9131b..191a9406fe 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -249,8 +249,6 @@ class DrawOccurrence(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): 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() diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index ecc1b86163..80c7ccc20e 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -1189,11 +1189,8 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato 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() diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index b26466aa15..4c3466d325 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -1697,17 +1697,6 @@ class ProductPreviewItem(PropertyGroup): value_2d: tuple[float, float] -class BIMProductPreviewProperties(PropertyGroup): - verts: bpy.props.CollectionProperty(type=ProductPreviewItem) - edges: bpy.props.CollectionProperty(type=ProductPreviewItem) - tris: bpy.props.CollectionProperty(type=ProductPreviewItem) - - if TYPE_CHECKING: - verts: bpy.types.bpy_prop_collection_idprop[ProductPreviewItem] - edges: bpy.types.bpy_prop_collection_idprop[ProductPreviewItem] - tris: bpy.types.bpy_prop_collection_idprop[ProductPreviewItem] - - def update_is_editing(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None: if self.is_editing: return diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 4c9e89bf41..b6046df18d 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -1135,11 +1135,8 @@ class DrawPolylineSlab(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): 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() diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index d652041125..d343bc75ae 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -787,11 +787,8 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): 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() diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 2f4c4b77b7..a81681652c 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -66,7 +66,6 @@ if TYPE_CHECKING: BIMRailingProperties, BIMExternalParametricGeometryProperties, BIMPolylineProperties, - BIMProductPreviewProperties, ) @@ -108,11 +107,6 @@ class Model(bonsai.core.tool.Model): assert (scene := bpy.context.scene) return scene.BIMPolylineProperties # pyright: ignore[reportAttributeAccessIssue] - @classmethod - def get_product_preview_props(cls) -> BIMProductPreviewProperties: - assert (scene := bpy.context.scene) - return scene.BIMProductPreviewProperties # pyright: ignore[reportAttributeAccessIssue] - @classmethod def convert_si_to_unit(cls, value: T) -> T: if isinstance(value, (tuple, list)): @@ -2750,3 +2744,20 @@ class Model(bonsai.core.tool.Model): list(nodes_to_update) finally: SvIfcStore.use_bonsai_file = False + + @classmethod + def create_bmesh_from_vertices(cls, vertices, is_closed=False): + bm = bmesh.new() + + new_verts = [bm.verts.new(v) for v in 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