From 46a31386da6dbd6611544430347a68fd4a57e87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Mar 2025 16:47:34 -0300 Subject: [PATCH] Add "Face Area" option to measure tool. See #6163. --- .../bonsai/bim/module/model/decorator.py | 70 ++++++++++++-- src/bonsai/bonsai/bim/module/model/prop.py | 1 + .../bonsai/bim/module/project/__init__.py | 1 + .../bonsai/bim/module/project/operator.py | 93 ++++++++++++++++++- src/bonsai/bonsai/bim/module/project/prop.py | 3 +- .../bonsai/bim/module/project/workspace.py | 5 +- src/bonsai/bonsai/tool/polyline.py | 2 + 7 files changed, 166 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 0a8c80b658..1919ffa97d 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -33,6 +33,7 @@ from gpu_extras.batch import batch_for_shader from gpu_extras.presets import draw_circle_2d from typing import Union from bonsai.bim.module.drawing.helper import format_distance +from itertools import chain def transparent_color(color, alpha=0.1): @@ -320,14 +321,15 @@ class PolylineDecorator: relating_type = None @classmethod - def install(cls, context): + def install(cls, context, ui_only=False): if cls.is_installed: cls.uninstall() handler = cls() - cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_snap_point, (context,), "WINDOW", "POST_PIXEL")) - cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_measurements, (context,), "WINDOW", "POST_PIXEL")) cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_ui, (context,), "WINDOW", "POST_PIXEL")) - cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")) + if not ui_only: + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_snap_point, (context,), "WINDOW", "POST_PIXEL")) + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_measurements, (context,), "WINDOW", "POST_PIXEL")) + cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")) cls.is_installed = True @classmethod @@ -376,6 +378,7 @@ class PolylineDecorator: return (x_axis, y_axis, z_axis), (x_middle, y_middle, z_middle) + @classmethod def calculate_polygon(self, points): bm = bmesh.new() @@ -389,8 +392,8 @@ class PolylineDecorator: bm.verts.index_update() bm.edges.index_update() - verts = bm.verts - edges = bm.edges + verts = [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()] bm.free() @@ -970,3 +973,58 @@ class SlabDirectionDecorator: base = [obj.matrix_world @ Vector(d) for d in base] self.draw_batch("LINES", dir, selected_elements_color, [(0, 1), (1, 2), (1, 3)]) self.draw_batch("LINES", base, selected_elements_color, [(0, 1)]) + + +class FaceAreaDecorator: + is_installed = False + handlers = [] + + @classmethod + def install(cls, context): + if cls.is_installed: + cls.uninstall() + handler = cls() + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_face_area, (context,), "WINDOW", "POST_VIEW")) + cls.is_installed = True + + @classmethod + def uninstall(cls): + for handler in cls.handlers: + try: + SpaceView3D.draw_handler_remove(handler, "WINDOW") + except ValueError: + pass + cls.is_installed = False + + def draw_batch(self, shader_type, content_pos, color, indices=None): + shader = self.line_shader if shader_type == "LINES" else self.shader + batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) + shader.uniform_float("color", color) + batch.draw(shader) + + def draw_face_area(self, context): + def transparent_color(color, alpha=0.1): + color = [i for i in color] + color[3] = alpha + return color + + self.addon_prefs = tool.Blender.get_addon_preferences() + self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR") + self.line_shader.bind() # required to be able to change uniforms of the shader + self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height)) + self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") + self.line_shader.uniform_float("lineWidth", 2.0) + gpu.state.point_size_set(6) + gpu.state.blend_set("ALPHA") + decorator_color = self.addon_prefs.decorator_color_special + + polyline_data = context.scene.BIMPolylineProperties.insertion_polyline + for i, polyline in enumerate(polyline_data): + vertices = [] + for point in polyline.polyline_points: + vertices.append((point.x, point.y, point.z)) + data = PolylineDecorator.calculate_polygon(vertices) + if data: + self.draw_batch("POINTS", data["verts"], decorator_color) + self.draw_batch("LINES", data["verts"], decorator_color, data["edges"]) + self.draw_batch("TRIS", data["verts"], transparent_color(decorator_color, alpha=0.5), data["tris"]) diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 3f7dc9475f..e5e56115fd 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -1119,6 +1119,7 @@ class PolylinePoint(PropertyGroup): class Polyline(PropertyGroup): + id: bpy.props.StringProperty(name="Id") polyline_points: bpy.props.CollectionProperty(type=PolylinePoint) measurement_type: bpy.props.StringProperty(name="Measurement Type") area: bpy.props.StringProperty(name="Measured Area") diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py index 6cc33bb7b9..81bf9e9ec7 100644 --- a/src/bonsai/bonsai/bim/module/project/__init__.py +++ b/src/bonsai/bonsai/bim/module/project/__init__.py @@ -48,6 +48,7 @@ classes = ( operator.LoadProject, operator.LoadProjectElements, operator.MeasureTool, + operator.MeasureFaceAreaTool, operator.ClearMeasurement, operator.NewProject, operator.QueryLinkedElement, diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 077ada3b6c..e84dabda74 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -61,7 +61,7 @@ from ifcopenshell.geom import ShapeElementType from bonsai.bim.module.project.data import LinksData, ProjectLibraryData from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator, MeasureDecorator from bonsai.bim.module.project.prop import BreadcrumbType -from bonsai.bim.module.model.decorator import PolylineDecorator +from bonsai.bim.module.model.decorator import PolylineDecorator, FaceAreaDecorator from bonsai.bim.module.model.polyline import PolylineOperator from typing import Union, TYPE_CHECKING, Literal, get_args @@ -2681,6 +2681,97 @@ class MeasureTool(bpy.types.Operator, PolylineOperator): return {"RUNNING_MODAL"} +class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator): + bl_idname = "bim.measure_face_area_tool" + bl_label = "Measure Face Area Tool" + bl_options = {"REGISTER", "UNDO"} + + measure_type: bpy.props.StringProperty() + + @classmethod + def poll(cls, context): + return context.space_data.type == "VIEW_3D" + + def __init__(self): + super().__init__() + self.input_options = ["AREA"] + self.input_ui = tool.Polyline.create_input_ui(input_options=self.input_options) + self.clicked_faces = [] + self.total_area = 0 + if tool.Ifc.get(): + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + else: + self.unit_scale = tool.Blender.get_unit_scale() + + def modal(self, context, event): + def select_face(mouse_pos): + objs_to_raycast = tool.Raycast.filter_objects_to_raycast(context, event, self.objs_2d_bbox) + obj, _, face_index = tool.Raycast.cast_rays_and_get_best_object( + context, event, objs_to_raycast, include_wireframes=False + ) + if face_index: + return obj, face_index + return None, None + + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + + if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: + self.handle_mouse_move(context, event) + return {"PASS_THROUGH"} + + self.handle_mouse_move(context, event) + + if event.value == "PRESS" and event.type == "LEFTMOUSE": + tool.Blender.update_viewport() + mouse_pos = event.mouse_region_x, event.mouse_region_y + obj, face_index = select_face(mouse_pos) + if face_index: + if obj.data.polygons[face_index] not in self.clicked_faces: + self.clicked_faces.append(obj.data.polygons[face_index]) + self.total_area += obj.data.polygons[face_index].area + self.input_ui.set_value("AREA", self.total_area) + polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline.add() + polyline_data.id = obj.name + str(face_index) + for v_id in obj.data.polygons[face_index].vertices: + vertex = obj.matrix_world @ obj.data.vertices[v_id].co + polyline_point = polyline_data.polyline_points.add() + polyline_point.x = vertex.x + polyline_point.y = vertex.y + polyline_point.z = vertex.z + tool.Blender.update_viewport() + + if event.shift and (event.value == "PRESS" and event.type == "LEFTMOUSE"): + mouse_pos = event.mouse_region_x, event.mouse_region_y + obj, face_index = select_face(mouse_pos) + if face_index: + if obj.data.polygons[face_index] in self.clicked_faces: + self.clicked_faces.remove(obj.data.polygons[face_index]) + self.total_area -= obj.data.polygons[face_index].area + self.input_ui.set_value("AREA", self.total_area) + polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline + for i, polyline in enumerate(polyline_data): + if polyline.id == obj.name + str(face_index): + polyline_data.remove(i) + tool.Blender.update_viewport() + + if event.value == "RELEASE" and event.type in {"ESC", "RIGHTMOUSE"}: + bpy.context.scene.BIMPolylineProperties.insertion_polyline.clear() + PolylineDecorator.uninstall() + FaceAreaDecorator.uninstall() + tool.Blender.update_viewport() + return {"CANCELLED"} + + return {"RUNNING_MODAL"} + + def invoke(self, context, event): + super().invoke(context, event) + PolylineDecorator.uninstall() + PolylineDecorator.install(context, ui_only=True) + FaceAreaDecorator.install(context) + return {"RUNNING_MODAL"} + + class ClearMeasurement(bpy.types.Operator): bl_idname = "bim.clear_measurement" bl_label = "Clear measurement from the screen" diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index a8ca1f2edd..3d57a7b926 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -499,7 +499,8 @@ class MeasureToolSettings(PropertyGroup): measurement_type_items = [ ("SINGLE", "SINGLE", "Single", "FIXED_SIZE", 1), ("POLYLINE", "POLYLINE", "Polyline", "DRIVER_ROTATIONAL_DIFFERENCE", 2), - ("AREA", "AREA", "Area", "OUTLINER_DATA_LIGHTPROBE", 3), + ("POLY_AREA", "POLY_AREA", "Poyline Area", "OUTLINER_DATA_LIGHTPROBE", 3), + ("FACE_AREA", "FACE_AREA", "Face Area", "FACESEL", 4), ] measurement_type: bpy.props.EnumProperty(items=measurement_type_items, default="POLYLINE") diff --git a/src/bonsai/bonsai/bim/module/project/workspace.py b/src/bonsai/bonsai/bim/module/project/workspace.py index bf3abe07c9..d58ad55adb 100644 --- a/src/bonsai/bonsai/bim/module/project/workspace.py +++ b/src/bonsai/bonsai/bim/module/project/workspace.py @@ -106,4 +106,7 @@ class ExploreHotkey(bpy.types.Operator): for obj in tool.Blender.get_selected_objects(): obj.select_set(False) measure_type = bpy.context.scene.MeasureToolSettings.measurement_type - bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type=measure_type) + if measure_type == "FACE_AREA": + bpy.ops.bim.measure_face_area_tool("INVOKE_DEFAULT") + else: + bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type=measure_type) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index ac9f741d3b..870cd1b278 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -66,6 +66,8 @@ class Polyline(bonsai.core.tool.Polyline): if attribute_name == "A": value = float(self.get_text_value(attribute_name)) return f"{value:.2f}°" + if attribute_name == "AREA": + return Polyline.format_input_ui_units(value, True) else: return Polyline.format_input_ui_units(value)