I propose to do it with a modal operator, not a gizmo:

The principles:
Run the operator (search it using its name F3 and type set origin on boundings
). Then the operator creates a sphere to display it as green point as shown above. And it register a draw handler in the 3D view, in order to display the boundings and the small green spheres.
The operator is a modal so it will continue until 'FINISHED'
or if 'CANCELED'
is returned. In this case, it returns 'CANCELED'
when the ESC key is hit. Now for each modal call, it does the following:
- Get the view location
- Get the mouse cursor location
- Get the active object
And from that the bounds of the object are going to be calculated in order to display a black subdivided mesh (some kind of lattice) as well as figuring out which point to highlight in green.
Here we can use the dot product the view location/mouse location with view location/bouding point location and keep the best one. When all that done, validating the point where to set the object origin:
If you left-click (LMB) then the previously identified bounding point will be used to change the origin. It seems to work fine independently of the mode (object, edit, etc.) but I've not tested them all.
You can stop the operator using ESC. Here is the code (some comments, just ask for more information if necessary):
import bpy
import bmesh
import gpu
from gpu_extras.batch import batch_for_shader
from mathutils import Vector, Matrix
from bpy_extras.view3d_utils import region_2d_to_location_3d
# indices to make cube edges from boundings
bounding_indices = (
(0, 1), (1, 2), (2, 3), (3, 4), #up
(4, 5), (5, 6), (6, 7), (7, 0),
(8, 1), (8, 3), (8, 5), (8, 7),
(9, 10), (10, 11), (11, 12), (12, 13), #slide
(13, 14), (14, 15), (15, 16), (16, 9), #down
(17, 18), (18, 19), (19, 20), (20, 21),
(21, 22),(22, 23), (23, 24), (24, 17),
(25, 18), (25, 20), (25, 22), (25, 24),
(0, 9), (1, 10), (2, 11), (3, 12), # transversal
(4, 13), (5, 14), (6,15), (7, 16),
(9, 17), (10, 18), (11, 19), (12, 20),
(13, 21), (14, 22), (15, 23), (16, 24))
# Cut a bounding face in half
def bounding_cuts(b_box, result, indices):
p0 = Vector(b_box[indices[0]])
p1 = Vector(b_box[indices[1]])
p2 = Vector(b_box[indices[2]])
p3 = Vector(b_box[indices[3]])
result.append(p0)
result.append((p0 + p1) / 2.0)
result.append(p1)
result.append((p1 + p2) / 2.0)
result.append(p2)
result.append((p2 + p3) / 2.0)
result.append(p3)
result.append((p3 + p0) / 2.0)
result.append((p0 + p1 + p2 + p3) / 4.0)
def up_down_slide(b_box):
up = []
down = []
slide = []
bounding_cuts(b_box, up, [0, 1, 2, 3])
bounding_cuts(b_box, down, [4, 5, 6, 7])
slide = [(p0+p1)/2.0 for p0,p1 in zip(up[:-1],down[:-1])]
return up, down, slide
# Get half parts of bounding box
def bounding_points(obj):
b_box = obj.bound_box[:]
up, down, slide = up_down_slide(b_box)
result = up + slide + down
return result
def draw_callback(self, context):
obj = context.active_object
if obj and obj.type == 'MESH':
# translate bounding in world co
mat = obj.matrix_world
obj_coords = [mat @ Vector(c) for c in bounding_points(obj)]
# draw boudings
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
shader.bind()
batch = batch_for_shader(shader, 'LINES', {"pos": obj_coords}, indices=bounding_indices)
shader.uniform_float("color", (0, 0, 0, 0.5))
batch.draw(shader)
# if close enough to a corner
if self.highlight != -1:
# draw a small sphere on it
scale = self.highlight_distance / 100.0
base_coord = obj_coords[self.highlight]
h_coords = [base_coord + (Vector(c) * scale) for c in self.highlight_coords]
batch = batch_for_shader(shader, 'TRIS', {"pos": h_coords})
shader.uniform_float("color", (0, 1, 0, 0.5))
batch.draw(shader)
class ModalOperator(bpy.types.Operator):
"""Set origin with the mouse"""
bl_idname = "object.set_origin_on_boundings"
bl_label = "Set origin on boundings"
# Create highlight (corner) geometry
def create_highlight(self):
# it is based on UV sphere triangulated for the GPU rendering
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments= 6, v_segments=4, diameter=1)
bmesh.ops.triangulate(bm, faces=bm.faces)
self.highlight_coords = [v.co.to_tuple() for f in bm.faces for v in f.verts]
# Update object information
def update_object(self, context, obj, view_loc, mouse_loc):
prev_highlight = self.highlight
self.highlight = -1
if obj and obj.type == 'MESH':
self.target = obj.name
# Find the closest corner using a dot product from the view
mat = obj.matrix_world
obj_coords = [mat @ Vector(c) for c in bounding_points(obj)]
axis = (mouse_loc - view_loc).normalized()
angles = [axis.dot((c - view_loc).normalized()) for c in obj_coords]
min_angle = max(angles)
# and keep it if close enough
if min_angle > 0.999:
self.highlight = angles.index(min_angle)
self.highlight_distance = (view_loc - obj_coords[self.highlight]).length
if self.highlight != prev_highlight:
context.area.tag_redraw()
# Mouse location in 3D
def mouse_location(self, context, event):
x, y = event.mouse_region_x, event.mouse_region_y
loc = region_2d_to_location_3d(context.region, context.space_data.region_3d, (x, y), (0, 0, 0))
return loc
# View location in 3D
def view_location(self, context):
camera_info = context.space_data.region_3d.view_matrix.inverted()
return camera_info.translation
# Move the object origin to the wanted corner
def set_origin(self, context, obj, index):
mat = obj.matrix_world
loc = mat @ Vector(bounding_points(obj)[index])
mode = obj.mode
me = obj.data
if mode == 'OBJECT':
mat0 =mat.copy()
t = loc - mat @ Vector()
mat.translation += t
local = mat.inverted() @ mat0 #move verts back
me.transform(local)
else:
local = mat.inverted() @ loc #move verts back (in prevision of next operation)
ml = Matrix.Translation(-local)
bm = bmesh.from_edit_mesh(me)
bm.transform(ml)
bmesh.update_edit_mesh(me)
t = loc - mat @ Vector()
mat.translation += t #move origin and verts
# Force the 3D view to redraw when needed
def redraw(self, context):
bpy.ops.wm.redraw_timer(type='DRAW_SWAP', iterations=1)
def modal(self, context, event):
mouse_loc = self.mouse_location(context, event)
view_loc = self.view_location(context)
obj = context.active_object
self.update_object(context, obj, view_loc, mouse_loc)
# when left click, set the origin if a highlight point is known
if event.type in {'LEFTMOUSE'}:
if self.highlight != -1:
self.set_origin(context, obj, self.highlight)
return {'RUNNING_MODAL'} #We don't want the click to be taken into account further
# esc is used to quit and remove the draw handler
elif event.type in {'ESC'}:
bpy.types.SpaceView3D.draw_handler_remove(self.handle, 'WINDOW')
context.area.tag_redraw()
return {'CANCELLED'}
return {'PASS_THROUGH'}
def invoke(self, context, event):
self.highlight = -1
if context.area.type == 'VIEW_3D':
self.create_highlight()
args = (self, context)
self.handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback, args, 'WINDOW', 'POST_VIEW')
context.window_manager.modal_handler_add(self)
context.area.tag_redraw()
return {'RUNNING_MODAL'}
return {'FINISHED'}
keymaps = []
def register():
bpy.utils.register_class(ModalOperator)
wm = bpy.context.window_manager
km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D")
# Ctrl+Shif+B as shortcut
kmi = km.keymap_items.new(ModalOperator.bl_idname, 'B', 'PRESS', ctrl=True, shift=True)
keymaps.append((km, kmi))
def unregister():
for km, kmi in keymaps:
km.keymap_items.remove(kmi)
keymaps.clear()
bpy.utils.unregister_class(ModalOperator)
if __name__ == "__main__":
register()
# test call
bpy.ops.object.set_origin_on_boundings('INVOKE_DEFAULT')

Run the script and use ctrlshiftB to activate the operator and esc to stop it.
Note: I'm still using right click select, so I've set the trigger that position the origin on the left click. You may want to change that.