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