diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 238a8ee5c6..44ed83bf7b 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -1123,3 +1123,204 @@ class FaceAreaDecorator: 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"]) + +class BoundingBoxDecorator: + is_installed = False + handlers = [] + + def __init__(self): + context = bpy.context + theme = context.preferences.themes.items()[0][1] + self.decorator_color_x_axis = (*theme.user_interface.axis_x, 1) + self.decorator_color_y_axis = (*theme.user_interface.axis_y, 1) + self.decorator_color_z_axis = (*theme.user_interface.axis_z, 1) + self.decorator_color_wire = (*theme.view_3d.bone_solid, 1) + self.decorator_color_special = tool.Blender.get_addon_preferences().decorator_color_special + + @classmethod + def install(cls, context): + if cls.is_installed: + cls.uninstall() + handler = cls() + cls.handlers.append( + bpy.types.SpaceView3D.draw_handler_add(handler.draw_bounding_box_wire_cube, (context,), "WINDOW", "POST_VIEW") + ) + cls.handlers.append( + bpy.types.SpaceView3D.draw_handler_add(handler.draw_dimension_text, (context,), "WINDOW", "POST_PIXEL") + ) + cls.is_installed = True + + @classmethod + def uninstall(cls): + for handler in cls.handlers: + try: + bpy.types.SpaceView3D.draw_handler_remove(handler, "WINDOW") + except Exception: + pass + cls.handlers.clear() + cls.is_installed = False + + @staticmethod + def get_combined_bounding_box_corners(objects): + import mathutils + if not objects: + return None, None, None + if len(objects) == 1: + obj = objects[0] + corners = [obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box] + else: + all_corners = [] + for obj in objects: + if hasattr(obj, "bound_box"): + all_corners.extend([obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box]) + min_corner = mathutils.Vector( + (min(v.x for v in all_corners), min(v.y for v in all_corners), min(v.z for v in all_corners)) + ) + max_corner = mathutils.Vector( + (max(v.x for v in all_corners), max(v.y for v in all_corners), max(v.z for v in all_corners)) + ) + corners = [ + mathutils.Vector((min_corner.x, min_corner.y, min_corner.z)), + mathutils.Vector((min_corner.x, min_corner.y, max_corner.z)), + mathutils.Vector((min_corner.x, max_corner.y, max_corner.z)), + mathutils.Vector((min_corner.x, max_corner.y, min_corner.z)), + mathutils.Vector((max_corner.x, min_corner.y, min_corner.z)), + mathutils.Vector((max_corner.x, min_corner.y, max_corner.z)), + mathutils.Vector((max_corner.x, max_corner.y, max_corner.z)), + mathutils.Vector((max_corner.x, max_corner.y, min_corner.z)), + ] + edges = [ + (0, 1, "Z"), (1, 2, "Y"), (2, 3, "Z"), (3, 0, "Y"), + (4, 5, "Z"), (5, 6, "Y"), (6, 7, "Z"), (7, 4, "Y"), + (0, 4, "X"), (1, 5, "X"), (2, 6, "X"), (3, 7, "X"), + ] + axis_colors = { + "X": (0.956, 0.282, 0.322, 1), + "Y": (0.565, 0.812, 0.125, 1), + "Z": (0.196, 0.529, 0.929, 1), + } + return corners, edges, axis_colors + + @staticmethod + def find_closest_trihedron(corners, edges, region, rv3d): + from bpy_extras.view3d_utils import location_3d_to_region_2d + min_y = float("inf") + best_origin = None + for idx, corner in enumerate(corners): + screen_co = location_3d_to_region_2d(region, rv3d, corner) + if screen_co is not None and screen_co.y < min_y: + min_y = screen_co.y + best_origin = idx + trihedron = [ + {"X": (0, 4), "Y": (0, 3), "Z": (0, 1)}, + {"X": (1, 5), "Y": (1, 2), "Z": (1, 0)}, + {"X": (2, 6), "Y": (2, 1), "Z": (2, 3)}, + {"X": (3, 7), "Y": (3, 0), "Z": (3, 2)}, + {"X": (4, 0), "Y": (4, 7), "Z": (4, 5)}, + {"X": (5, 1), "Y": (5, 6), "Z": (5, 4)}, + {"X": (6, 2), "Y": (6, 5), "Z": (6, 7)}, + {"X": (7, 3), "Y": (7, 4), "Z": (7, 6)}, + ] + return trihedron[best_origin] + + def draw_batch(self, shader_type, content_pos, color, indices=None): + if not tool.Blender.validate_shader_batch_data(content_pos, indices): + return + 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_text_background(self, context, coords_dim, text_dim): + padding = 5 + theme = context.preferences.themes.items()[0][1] + color = (*theme.user_interface.wcol_menu_back.inner[:3], 0.5) # unwrap color values and adds alpha + top_left = (coords_dim[0] - padding, coords_dim[1] + text_dim[1] + padding) + bottom_left = (coords_dim[0] - padding, coords_dim[1] - padding) + top_right = (coords_dim[0] + text_dim[0] + padding, coords_dim[1] + text_dim[1] + padding) + bottom_right = (coords_dim[0] + text_dim[0] + padding, coords_dim[1] - padding) + + verts = [top_left, bottom_left, top_right, bottom_right] + gpu.state.blend_set("ALPHA") + self.draw_batch("TRIS", verts, color, [(0, 1, 2), (1, 2, 3)]) + + def draw_bounding_box_wire_cube(self, context): + import gpu + from gpu_extras.batch import batch_for_shader + + self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR") + self.line_shader.bind() + self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height)) + self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") + + selected_objects = [obj for obj in bpy.context.selected_objects if hasattr(obj, "bound_box")] + if not selected_objects: + return + corners, edges, axis_colors = self.get_combined_bounding_box_corners(selected_objects) + region = bpy.context.region + rv3d = bpy.context.region_data + gpu.state.line_width_set(2.0) + for i1, i2, axis in edges: + self.draw_batch("LINES", [corners[i1], corners[i2]], self.decorator_color_wire) + closest_indices = self.find_closest_trihedron(corners, edges, region, rv3d) + for axis in "XYZ": + pair = closest_indices[axis] + if pair is not None: + i1, i2 = pair + color = getattr(self, f"decorator_color_{axis.lower()}_axis") + self.draw_batch("LINES", [corners[i1], corners[i2]], color) + + def draw_dimension_text(self, context): + from bpy_extras.view3d_utils import location_3d_to_region_2d + from bonsai.bim.module.drawing.helper import format_distance + import mathutils + import blf + + self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR") + self.line_shader.bind() + self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height)) + self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") + + + selected_objects = [obj for obj in bpy.context.selected_objects if hasattr(obj, "bound_box")] + if not selected_objects: + return + corners, edges, axis_colors = self.get_combined_bounding_box_corners(selected_objects) + if not corners: + return + dims = mathutils.Vector( + ( + (corners[4] - corners[0]).length, + (corners[3] - corners[0]).length, + (corners[1] - corners[0]).length, + ) + ) + region = bpy.context.region + rv3d = bpy.context.region_data + + # Preferences + addon_prefs = tool.Blender.get_addon_preferences() + font_id = 0 + font_size = tool.Blender.scale_font_size(12) + blf.size(font_id, font_size) + blf.enable(font_id, blf.SHADOW) + blf.shadow(font_id, 6, 0, 0, 0, 1) + color = addon_prefs.decorations_colour + blf.color(font_id, *color) + + closest_indices = self.find_closest_trihedron(corners, edges, region, rv3d) + for axis in "XYZ": + pair = closest_indices[axis] + if pair is not None: + i1, i2 = pair + center = (corners[i1] + corners[i2]) / 2 + value = getattr(dims, axis.lower()) + screen_co = location_3d_to_region_2d(region, rv3d, center) + if screen_co is not None: + blf.position(font_id, screen_co.x, screen_co.y, 0) + blf.color(font_id, 1, 1, 1, 1) + value_str = f"D{axis.lower()}: " + format_distance(value, hide_units=False) + text_length = blf.dimensions(font_id, value_str) + self.draw_text_background(context, screen_co, text_length) + blf.draw(font_id, value_str) + blf.disable(font_id, blf.SHADOW) \ No newline at end of file diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 2648b76291..40a31d88f5 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2791,267 +2791,6 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator): FaceAreaDecorator.install(context) return {"RUNNING_MODAL"} - -class MeasureXYZDimensionsTool(bpy.types.Operator): - bl_idname = "bim.measure_xyz_dimensions_tool" - bl_label = "Show bonding box dimensions" - bl_options = {"REGISTER", "UNDO"} - - _draw_handler_box = None - _draw_handler_text = None - previous_selection = set() - - @staticmethod - def get_combined_bounding_box_corners(objects): - import mathutils - - if not objects: - return None, None, None - if len(objects) == 1: - obj = bpy.context.active_object - corners = [obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box] - else: - all_corners = [] - for obj in objects: - if hasattr(obj, "bound_box"): - all_corners.extend([obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box]) - min_corner = mathutils.Vector( - (min(v.x for v in all_corners), min(v.y for v in all_corners), min(v.z for v in all_corners)) - ) - max_corner = mathutils.Vector( - (max(v.x for v in all_corners), max(v.y for v in all_corners), max(v.z for v in all_corners)) - ) - corners = [ - mathutils.Vector((min_corner.x, min_corner.y, min_corner.z)), - mathutils.Vector((min_corner.x, min_corner.y, max_corner.z)), - mathutils.Vector((min_corner.x, max_corner.y, max_corner.z)), - mathutils.Vector((min_corner.x, max_corner.y, min_corner.z)), - mathutils.Vector((max_corner.x, min_corner.y, min_corner.z)), - mathutils.Vector((max_corner.x, min_corner.y, max_corner.z)), - mathutils.Vector((max_corner.x, max_corner.y, max_corner.z)), - mathutils.Vector((max_corner.x, max_corner.y, min_corner.z)), - ] - edges = [ - (0, 1, "Z"), - (1, 2, "Y"), - (2, 3, "Z"), - (3, 0, "Y"), - (4, 5, "Z"), - (5, 6, "Y"), - (6, 7, "Z"), - (7, 4, "Y"), - (0, 4, "X"), - (1, 5, "X"), - (2, 6, "X"), - (3, 7, "X"), - ] - axis_colors = { - "X": (0.956, 0.282, 0.322, 1), - "Y": (0.565, 0.812, 0.125, 1), - "Z": (0.196, 0.529, 0.929, 1), - } - return corners, edges, axis_colors - - @staticmethod - def find_closest_trihedron(corners, edges, region, rv3d): - from bpy_extras.view3d_utils import location_3d_to_region_2d - - min_y = float("inf") - best_origin = None - for idx, corner in enumerate(corners): - screen_co = location_3d_to_region_2d(region, rv3d, corner) - if screen_co is not None and screen_co.y < min_y: - min_y = screen_co.y - best_origin = idx - trihedron = [ - {"X": (0, 4), "Y": (0, 3), "Z": (0, 1)}, - {"X": (1, 5), "Y": (1, 2), "Z": (1, 0)}, - {"X": (2, 6), "Y": (2, 1), "Z": (2, 3)}, - {"X": (3, 7), "Y": (3, 0), "Z": (3, 2)}, - {"X": (4, 0), "Y": (4, 7), "Z": (4, 5)}, - {"X": (5, 1), "Y": (5, 6), "Z": (5, 4)}, - {"X": (6, 2), "Y": (6, 5), "Z": (6, 7)}, - {"X": (7, 3), "Y": (7, 4), "Z": (7, 6)}, - ] - return trihedron[best_origin] - - @classmethod - def draw_bounding_box_wire_cube(cls): - selected_objects = [obj for obj in bpy.context.selected_objects if hasattr(obj, "bound_box")] - if not selected_objects: - return - corners, edges, axis_colors = cls.get_combined_bounding_box_corners(selected_objects) - region = bpy.context.region - rv3d = bpy.context.region_data - shader = gpu.shader.from_builtin("UNIFORM_COLOR") - gpu.state.line_width_set(2.0) - for i1, i2, axis in edges: - color = (0.5, 0.5, 0.5, 0.75) - shader.bind() - shader.uniform_float("color", color) - batch = batch_for_shader(shader, "LINES", {"pos": [corners[i1], corners[i2]]}) - batch.draw(shader) - closest_indices = cls.find_closest_trihedron(corners, edges, region, rv3d) - for axis in "XYZ": - pair = closest_indices[axis] - if pair is not None: - i1, i2 = pair - color = axis_colors[axis] - shader.bind() - shader.uniform_float("color", color) - batch = batch_for_shader(shader, "LINES", {"pos": [corners[i1], corners[i2]]}) - batch.draw(shader) - - @classmethod - def draw_batch(self, shader_type, content_pos, color, indices=None): - if not tool.Blender.validate_shader_batch_data(content_pos, indices): - return - # shader = self.line_shader if shader_type == "LINES" else self.shader - shader = gpu.shader.from_builtin("UNIFORM_COLOR") - batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) - shader.uniform_float("color", color) - batch.draw(shader) - - @classmethod - def draw_text_background(self, context, coords_dim, text_dim): - padding = 5 - theme = context.preferences.themes.items()[0][1] - color = (*theme.user_interface.wcol_menu_back.inner[:3], 0.5) # unwrap color values and adds alpha - top_left = (coords_dim[0] - padding, coords_dim[1] + text_dim[1] + padding) - bottom_left = (coords_dim[0] - padding, coords_dim[1] - padding) - top_right = (coords_dim[0] + text_dim[0] + padding, coords_dim[1] + text_dim[1] + padding) - bottom_right = (coords_dim[0] + text_dim[0] + padding, coords_dim[1] - padding) - - verts = [top_left, bottom_left, top_right, bottom_right] - gpu.state.blend_set("ALPHA") - self.draw_batch("TRIS", verts, color, [(0, 1, 2), (1, 2, 3)]) - - @classmethod - def draw_dimension_text(cls): - from bpy_extras.view3d_utils import location_3d_to_region_2d - from bonsai.bim.module.drawing.helper import format_distance - - selected_objects = [obj for obj in bpy.context.selected_objects if hasattr(obj, "bound_box")] - if not selected_objects: - return - corners, edges, axis_colors = cls.get_combined_bounding_box_corners(selected_objects) - if not corners: - return - dims = mathutils.Vector( - ( - (corners[4] - corners[0]).length, - (corners[3] - corners[0]).length, - (corners[1] - corners[0]).length, - ) - ) - region = bpy.context.region - rv3d = bpy.context.region_data - closest_indices = cls.find_closest_trihedron(corners, edges, region, rv3d) - - addon_prefs = tool.Blender.get_addon_preferences() - font_id = 0 - font_size = tool.Blender.scale_font_size(12) - blf.size(font_id, font_size) - blf.enable(font_id, blf.SHADOW) - blf.shadow(font_id, 6, 0, 0, 0, 1) - color = addon_prefs.decorations_colour - blf.color(font_id, *color) - - for axis in "XYZ": - pair = closest_indices[axis] - if pair is not None: - i1, i2 = pair - center = (corners[i1] + corners[i2]) / 2 - value = getattr(dims, axis.lower()) - screen_co = location_3d_to_region_2d(region, rv3d, center) - if screen_co is not None: - blf.position(font_id, screen_co.x, screen_co.y, 0) - blf.color(font_id, 1, 1, 1, 1) - value_str = f"D{axis.lower()}: " + format_distance(value, hide_units=False) - text_length = blf.dimensions(font_id, value_str) - cls.draw_text_background(bpy.context, screen_co, text_length) - blf.draw(font_id, value_str) - - @classmethod - def enable_bounding_box_wire_cube(cls): - if cls._draw_handler_box is None: - cls._draw_handler_box = bpy.types.SpaceView3D.draw_handler_add( - cls.draw_bounding_box_wire_cube, (), "WINDOW", "POST_VIEW" - ) - if cls._draw_handler_text is None: - cls._draw_handler_text = bpy.types.SpaceView3D.draw_handler_add( - cls.draw_dimension_text, (), "WINDOW", "POST_PIXEL" - ) - - @classmethod - def disable_bounding_box_wire_cube(cls): - if cls._draw_handler_box is not None: - bpy.types.SpaceView3D.draw_handler_remove(cls._draw_handler_box, "WINDOW") - cls._draw_handler_box = None - if cls._draw_handler_text is not None: - bpy.types.SpaceView3D.draw_handler_remove(cls._draw_handler_text, "WINDOW") - cls._draw_handler_text = None - - @classmethod - def selection_monitor_handler(cls, scene, depsgraph): - current_selection = {obj.name for obj in scene.objects if obj.select_get()} - if current_selection != cls.previous_selection: - if len(bpy.context.selected_objects) == 1: - scene.transform_orientation_slots[1].type = "LOCAL" - else: - scene.transform_orientation_slots[1].type = "GLOBAL" - area = next((a for a in bpy.context.screen.areas if a.type == "VIEW_3D"), None) - space = next((s for s in area.spaces if s.type == "VIEW_3D"), None) - space.show_gizmo_object_translate = True - cls.previous_selection = current_selection - - @classmethod - def register_handler(cls): - cls.unregister_handler() - bpy.app.handlers.depsgraph_update_post.append(cls.selection_monitor_handler) - - @classmethod - def unregister_handler(cls): - handlers = bpy.app.handlers.depsgraph_update_post - handlers[:] = [h for h in handlers if h.__name__ != "selection_monitor_handler"] - - def execute(self, context): - scene = bpy.context.scene - area_3d = next((area for area in bpy.context.screen.areas if area.type == "VIEW_3D"), None) - space_3d = next((space for space in area_3d.spaces if space.type == "VIEW_3D"), None) if area_3d else None - try: - if bpy.context.scene.MeasureToolSettings.measurement_type == "XYZ_DIMENSIONS": - self.enable_bounding_box_wire_cube() - self.register_handler() - if not scene.get("prev_transform_orientation_slot_type", None): - scene["prev_transform_orientation_slot_type"] = scene.transform_orientation_slots[1].type - if not scene.get("prev_show_gizmo_object_translate", None): - scene["prev_show_gizmo_object_translate"] = ( - space_3d.show_gizmo_object_translate if space_3d else False - ) - if space_3d: - if len(context.selected_objects) == 1: - scene.transform_orientation_slots[1].type = "LOCAL" - else: - scene.transform_orientation_slots[1].type = "GLOBAL" - space_3d.show_gizmo_object_translate = True - else: - self.disable_bounding_box_wire_cube() - self.unregister_handler() - if space_3d: - prev_orientation = scene.get("prev_transform_orientation_slot_type", None) - if prev_orientation: - scene.transform_orientation_slots[1].type = prev_orientation - scene["prev_transform_orientation_slot_type"] = "" - prev_gizmo = scene.get("prev_show_gizmo_object_translate", None) - if prev_gizmo is not None: - space_3d.show_gizmo_object_translate = prev_gizmo - scene["prev_show_gizmo_object_translate"] = None - except Exception: - self.report({"WARNING"}, "Could not set transform orientation.") - return {"FINISHED"} - - class ClearMeasurement(bpy.types.Operator): bl_idname = "bim.clear_measurement" bl_label = "Clear measurement from the screen" @@ -3066,3 +2805,43 @@ class ClearMeasurement(bpy.types.Operator): MeasureDecorator.uninstall() tool.Blender.update_viewport() return {"FINISHED"} + +from bonsai.bim.module.model.decorator import BoundingBoxDecorator + +class MeasureXYZDimensionsTool(bpy.types.Operator): + bl_idname = "bim.measure_xyz_dimensions_tool" + bl_label = "Show bounding box dimensions" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + scene = context.scene + area_3d = next((area for area in context.screen.areas if area.type == "VIEW_3D"), None) + space_3d = next((space for space in area_3d.spaces if space.type == "VIEW_3D"), None) if area_3d else None + + if scene.MeasureToolSettings.measurement_type == "XYZ_DIMENSIONS": + BoundingBoxDecorator.install(context) + # Save previous state if not already saved + if not scene.get("prev_transform_orientation_slot_type", None): + scene["prev_transform_orientation_slot_type"] = scene.transform_orientation_slots[1].type + if not scene.get("prev_show_gizmo_object_translate", None) and space_3d: + scene["prev_show_gizmo_object_translate"] = space_3d.show_gizmo_object_translate + # Set new state + if space_3d: + if len(context.selected_objects) == 1: + scene.transform_orientation_slots[1].type = "LOCAL" + else: + scene.transform_orientation_slots[1].type = "GLOBAL" + space_3d.show_gizmo_object_translate = True + else: + BoundingBoxDecorator.uninstall() + # Restore previous state + if space_3d: + prev_orientation = scene.get("prev_transform_orientation_slot_type", None) + if prev_orientation: + scene.transform_orientation_slots[1].type = prev_orientation + scene["prev_transform_orientation_slot_type"] = "" + prev_gizmo = scene.get("prev_show_gizmo_object_translate", None) + if prev_gizmo is not None: + space_3d.show_gizmo_object_translate = prev_gizmo + scene["prev_show_gizmo_object_translate"] = None + return {"FINISHED"} \ No newline at end of file