212 lines
No EOL
7.9 KiB
Python
212 lines
No EOL
7.9 KiB
Python
bl_info = {
|
|
"name": "Selection Outline",
|
|
"author": "ChatGPT + Ryan",
|
|
"version": (1, 0),
|
|
"blender": (3, 0, 0),
|
|
"location": "View3D > Sidebar > Outline",
|
|
"description": "Draw a custom outline around selected objects",
|
|
"category": "3D View",
|
|
}
|
|
|
|
import bpy
|
|
import gpu
|
|
import bmesh
|
|
from gpu_extras.batch import batch_for_shader
|
|
from mathutils import Vector
|
|
from bpy.app.handlers import persistent
|
|
|
|
def draw_callback():
|
|
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
|
|
gpu.state.blend_set('ALPHA')
|
|
gpu.state.line_width_set(bpy.context.window_manager.outline_thickness)
|
|
gpu.state.depth_test_set('NONE')
|
|
|
|
selected = bpy.context.selected_objects
|
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
|
|
color_obj = bpy.context.window_manager.outline_color
|
|
|
|
|
|
for obj in selected:
|
|
if obj.type == 'MESH':
|
|
if bpy.context.mode == 'EDIT_MESH' and obj == bpy.context.active_object:
|
|
bm = bmesh.from_edit_mesh(obj.data)
|
|
edges = [e for e in bm.edges if e.select]
|
|
if not edges:
|
|
continue
|
|
coords = []
|
|
for e in edges:
|
|
v1 = obj.matrix_world @ e.verts[0].co
|
|
v2 = obj.matrix_world @ e.verts[1].co
|
|
coords.extend([v1, v2])
|
|
shader.bind()
|
|
shader.uniform_float("color", color_obj)
|
|
batch = batch_for_shader(shader, 'LINES', {"pos": coords})
|
|
batch.draw(shader)
|
|
else:
|
|
eval_obj = obj.evaluated_get(depsgraph)
|
|
mesh = eval_obj.to_mesh()
|
|
if not mesh:
|
|
continue
|
|
coords = [obj.matrix_world @ v.co for v in mesh.vertices]
|
|
edges = [(e.vertices[0], e.vertices[1]) for e in mesh.edges]
|
|
verts = [coords[i] for edge in edges for i in edge]
|
|
shader.bind()
|
|
shader.uniform_float("color", color_obj)
|
|
batch = batch_for_shader(shader, 'LINES', {"pos": verts})
|
|
batch.draw(shader)
|
|
eval_obj.to_mesh_clear()
|
|
|
|
elif obj.type in {'CURVE', 'SURFACE', 'FONT'}:
|
|
eval_obj = obj.evaluated_get(depsgraph)
|
|
mesh = eval_obj.to_mesh()
|
|
if not mesh:
|
|
continue
|
|
coords = [obj.matrix_world @ v.co for v in mesh.vertices]
|
|
edges = [(e.vertices[0], e.vertices[1]) for e in mesh.edges]
|
|
verts = [coords[i] for edge in edges for i in edge]
|
|
shader.bind()
|
|
shader.uniform_float("color", color_obj)
|
|
batch = batch_for_shader(shader, 'LINES', {"pos": verts})
|
|
batch.draw(shader)
|
|
eval_obj.to_mesh_clear()
|
|
|
|
elif obj.type == 'EMPTY':
|
|
size = obj.empty_display_size
|
|
mat = obj.matrix_world
|
|
half = size * 0.5
|
|
corners = [Vector((x, y, z)) for x in (-half, half) for y in (-half, half) for z in (-half, half)]
|
|
coords = [mat @ corner for corner in corners]
|
|
edges = [
|
|
(0, 1), (0, 2), (0, 4),
|
|
(1, 3), (1, 5),
|
|
(2, 3), (2, 6),
|
|
(3, 7),
|
|
(4, 5), (4, 6),
|
|
(5, 7),
|
|
(6, 7),
|
|
]
|
|
verts = [coords[i] for edge in edges for i in edge]
|
|
shader.bind()
|
|
shader.uniform_float("color", color_obj)
|
|
batch = batch_for_shader(shader, 'LINES', {"pos": verts})
|
|
batch.draw(shader)
|
|
|
|
class VIEW3D_OT_draw_selection_outline(bpy.types.Operator):
|
|
"""Toggle selection outline"""
|
|
bl_idname = "view3d.draw_selection_outline"
|
|
bl_label = "Toggle Outline Drawing"
|
|
bl_options = {'REGISTER'}
|
|
|
|
_handle = None
|
|
|
|
def execute(self, context):
|
|
wm = context.window_manager
|
|
if wm.draw_selection_outline:
|
|
# Turn off
|
|
if self.__class__._handle is not None:
|
|
bpy.types.SpaceView3D.draw_handler_remove(self.__class__._handle, 'WINDOW')
|
|
self.__class__._handle = None
|
|
wm.draw_selection_outline = False
|
|
self.report({'INFO'}, "Selection outline stopped")
|
|
return {'FINISHED'}
|
|
else:
|
|
# Turn on
|
|
if self.__class__._handle is None:
|
|
self.__class__._handle = bpy.types.SpaceView3D.draw_handler_add(
|
|
draw_callback, (), 'WINDOW', 'POST_VIEW'
|
|
)
|
|
wm.draw_selection_outline = True
|
|
context.window_manager.modal_handler_add(self)
|
|
self.report({'INFO'}, "Selection outline started")
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def modal(self, context, event):
|
|
if context.area:
|
|
context.area.tag_redraw()
|
|
if not context.window_manager.draw_selection_outline:
|
|
return {'CANCELLED'}
|
|
return {'PASS_THROUGH'}
|
|
|
|
|
|
class VIEW3D_PT_selection_outline(bpy.types.Panel):
|
|
bl_label = "Selection Outline"
|
|
bl_space_type = 'VIEW_3D'
|
|
bl_region_type = 'UI'
|
|
bl_category = "Outline"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
wm = context.window_manager
|
|
|
|
row = layout.row()
|
|
if wm.draw_selection_outline:
|
|
row.operator("view3d.draw_selection_outline", text="Disable Outline", icon='CANCEL')
|
|
else:
|
|
row.operator("view3d.draw_selection_outline", text="Enable Outline", icon='RESTRICT_VIEW_OFF')
|
|
|
|
layout.prop(wm, "outline_thickness")
|
|
layout.prop(wm, "outline_color", text="Object Color")
|
|
|
|
|
|
@persistent
|
|
def enable_outline_on_load(dummy):
|
|
"""Automatically enable outline drawing when Blender starts or loads a file"""
|
|
# Use a timer to ensure everything is properly initialized
|
|
bpy.app.timers.register(lambda: start_outline_drawing(), first_interval=0.1)
|
|
|
|
|
|
def start_outline_drawing():
|
|
"""Start the outline drawing"""
|
|
wm = bpy.context.window_manager
|
|
if not wm.draw_selection_outline:
|
|
# Enable the outline
|
|
if VIEW3D_OT_draw_selection_outline._handle is None:
|
|
VIEW3D_OT_draw_selection_outline._handle = bpy.types.SpaceView3D.draw_handler_add(
|
|
draw_callback, (), 'WINDOW', 'POST_VIEW'
|
|
)
|
|
wm.draw_selection_outline = True
|
|
print("Selection outline auto-enabled")
|
|
return None # Don't repeat the timer
|
|
|
|
|
|
def register():
|
|
bpy.utils.register_class(VIEW3D_OT_draw_selection_outline)
|
|
bpy.utils.register_class(VIEW3D_PT_selection_outline)
|
|
|
|
bpy.types.WindowManager.draw_selection_outline = bpy.props.BoolProperty(default=False)
|
|
bpy.types.WindowManager.outline_thickness = bpy.props.FloatProperty(
|
|
name="Line Thickness", default=6.0, min=1.0, max=100.0
|
|
)
|
|
bpy.types.WindowManager.outline_color = bpy.props.FloatVectorProperty(
|
|
name="Object Color", subtype='COLOR', size=4,
|
|
min=0.0, max=1.0, default=(0.04, 1.0, 0, 1.0)
|
|
)
|
|
|
|
# Add the handler to auto-enable on startup
|
|
bpy.app.handlers.load_post.append(enable_outline_on_load)
|
|
|
|
# Also enable it immediately when the addon is first registered
|
|
if bpy.context.window_manager is not None:
|
|
bpy.app.timers.register(lambda: start_outline_drawing(), first_interval=0.1)
|
|
|
|
|
|
def unregister():
|
|
# Remove the load handler
|
|
if enable_outline_on_load in bpy.app.handlers.load_post:
|
|
bpy.app.handlers.load_post.remove(enable_outline_on_load)
|
|
|
|
# Stop the drawing if active
|
|
if VIEW3D_OT_draw_selection_outline._handle is not None:
|
|
bpy.types.SpaceView3D.draw_handler_remove(VIEW3D_OT_draw_selection_outline._handle, 'WINDOW')
|
|
VIEW3D_OT_draw_selection_outline._handle = None
|
|
|
|
bpy.utils.unregister_class(VIEW3D_OT_draw_selection_outline)
|
|
bpy.utils.unregister_class(VIEW3D_PT_selection_outline)
|
|
|
|
del bpy.types.WindowManager.draw_selection_outline
|
|
del bpy.types.WindowManager.outline_thickness
|
|
del bpy.types.WindowManager.outline_color
|
|
|
|
if __name__ == "__main__":
|
|
register() |