Separate decorator from operator to follow other modules in model / project

This commit is contained in:
falken10
2025-05-19 19:05:47 +02:00
committed by Bruno Perdigão
parent 3557b11e64
commit f5db905223
2 changed files with 241 additions and 261 deletions
@@ -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)
+40 -261
View File
@@ -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"}