157 lines
5.1 KiB
Python
157 lines
5.1 KiB
Python
bl_info = {
|
|
"name": "Infinite Zoom",
|
|
"author": "ChatGPT + You",
|
|
"version": (1, 1),
|
|
"blender": (3, 0, 0),
|
|
"location": "View3D > Sidebar > View > Infinite Zoom",
|
|
"description": "Zoom toward cursor with adjustable non-linear scaling and UI status",
|
|
"category": "3D View"
|
|
}
|
|
|
|
import bpy
|
|
import bpy_extras.view3d_utils as view3d_utils
|
|
|
|
# -------------------------------------------------------------------
|
|
# Properties
|
|
# -------------------------------------------------------------------
|
|
|
|
class CursorZoomSettings(bpy.types.PropertyGroup):
|
|
base_speed: bpy.props.FloatProperty(
|
|
name="Base Speed",
|
|
default=0.01,
|
|
min=0.001,
|
|
max=10.0,
|
|
step=0.1
|
|
)
|
|
exponent: bpy.props.FloatProperty(
|
|
name="Exponent",
|
|
default=1.3,
|
|
min=0.1,
|
|
max=10.0,
|
|
step=0.1
|
|
)
|
|
min_step: bpy.props.FloatProperty(
|
|
name="Minimum Step",
|
|
default=0.001,
|
|
min=0.00001,
|
|
max=1.0,
|
|
step=0.1
|
|
)
|
|
is_zoom_running: bpy.props.BoolProperty(
|
|
name="Zoom Running",
|
|
default=False
|
|
)
|
|
|
|
# -------------------------------------------------------------------
|
|
# Modal Zoom Operator
|
|
# -------------------------------------------------------------------
|
|
|
|
class VIEW3D_OT_cursor_zoom_modal(bpy.types.Operator):
|
|
bl_idname = "view3d.cursor_zoom_modal"
|
|
bl_label = "Cursor Zoom Modal"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def modal(self, context, event):
|
|
settings = context.scene.cursor_zoom_settings
|
|
context.area.tag_redraw()
|
|
|
|
if event.type in {'ESC', 'RIGHTMOUSE'}:
|
|
settings.is_zoom_running = False
|
|
return {'CANCELLED'}
|
|
|
|
if event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
|
|
zoom_direction = 1 if event.type == 'WHEELUPMOUSE' else -1
|
|
region = context.region
|
|
region_3d = context.space_data.region_3d
|
|
mouse_coord = (event.mouse_region_x, event.mouse_region_y)
|
|
|
|
view_vector = view3d_utils.region_2d_to_vector_3d(region, region_3d, mouse_coord)
|
|
|
|
center = (region.width // 2, region.height // 2)
|
|
offset = (center[0] + 100, center[1])
|
|
|
|
p1 = view3d_utils.region_2d_to_location_3d(region, region_3d, center, region_3d.view_location)
|
|
p2 = view3d_utils.region_2d_to_location_3d(region, region_3d, offset, region_3d.view_location)
|
|
screen_scale = (p2 - p1).length
|
|
|
|
zoom_step = (screen_scale ** settings.exponent) * settings.base_speed
|
|
zoom_step = max(zoom_step, settings.min_step)
|
|
|
|
move_vector = view_vector.normalized() * zoom_direction * zoom_step
|
|
region_3d.view_location += move_vector
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
return {'PASS_THROUGH'}
|
|
|
|
def invoke(self, context, event):
|
|
if context.area.type == 'VIEW_3D':
|
|
context.scene.cursor_zoom_settings.is_zoom_running = True
|
|
context.window_manager.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
else:
|
|
self.report({'WARNING'}, "3D View not found")
|
|
return {'CANCELLED'}
|
|
|
|
# -------------------------------------------------------------------
|
|
# Stop Operator
|
|
# -------------------------------------------------------------------
|
|
|
|
class VIEW3D_OT_stop_cursor_zoom(bpy.types.Operator):
|
|
bl_idname = "view3d.stop_cursor_zoom"
|
|
bl_label = "Stop Zoom"
|
|
|
|
def execute(self, context):
|
|
context.scene.cursor_zoom_settings.is_zoom_running = False
|
|
return {'FINISHED'}
|
|
|
|
# -------------------------------------------------------------------
|
|
# UI Panel
|
|
# -------------------------------------------------------------------
|
|
|
|
class VIEW3D_PT_cursor_zoom_panel(bpy.types.Panel):
|
|
bl_label = "Infinite Zoom"
|
|
bl_idname = "VIEW3D_PT_cursor_zoom_panel"
|
|
bl_space_type = 'VIEW_3D'
|
|
bl_region_type = 'UI'
|
|
bl_category = 'View'
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
settings = context.scene.cursor_zoom_settings
|
|
|
|
layout.prop(settings, "base_speed")
|
|
layout.prop(settings, "exponent")
|
|
layout.prop(settings, "min_step")
|
|
|
|
layout.separator()
|
|
if settings.is_zoom_running:
|
|
layout.operator("view3d.stop_cursor_zoom", text="Stop Zoom", icon='PAUSE')
|
|
layout.label(text="Zoom is ACTIVE", icon='CHECKMARK')
|
|
else:
|
|
layout.operator("view3d.cursor_zoom_modal", text="Start Zoom", icon='PLAY')
|
|
layout.label(text="Zoom is INACTIVE", icon='INFO')
|
|
|
|
# -------------------------------------------------------------------
|
|
# Register / Unregister
|
|
# -------------------------------------------------------------------
|
|
|
|
classes = (
|
|
CursorZoomSettings,
|
|
VIEW3D_OT_cursor_zoom_modal,
|
|
VIEW3D_OT_stop_cursor_zoom,
|
|
VIEW3D_PT_cursor_zoom_panel,
|
|
)
|
|
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
bpy.types.Scene.cursor_zoom_settings = bpy.props.PointerProperty(type=CursorZoomSettings)
|
|
|
|
def unregister():
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls)
|
|
del bpy.types.Scene.cursor_zoom_settings
|
|
|
|
if __name__ == "__main__":
|
|
register()
|