Utility_Apps/Blender/addons/Subdivide at Cursor/__init__.py

278 lines
No EOL
9.4 KiB
Python

bl_info = {
"name": "Subdivide at Cursor",
"author": "Claude",
"version": (1, 0),
"blender": (3, 0, 0),
"location": "View3D > Edit Mode > Edge Menu",
"description": "Subdivide nearest edge with new vertex at mouse cursor position",
"category": "Mesh",
}
import bpy
import bmesh
from bpy_extras import view3d_utils
from mathutils import Vector
import math
class MESH_OT_subdivide_at_cursor(bpy.types.Operator):
"""Subdivide nearest edge with new vertex near mouse cursor"""
bl_idname = "mesh.subdivide_at_cursor"
bl_label = "Subdivide at Cursor"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.mode == 'EDIT_MESH' and
context.object is not None and
context.object.type == 'MESH')
def invoke(self, context, event):
self.mouse_pos = (event.mouse_region_x, event.mouse_region_y)
return self.execute(context)
def execute(self, context):
obj = context.object
mesh = obj.data
# Get the region and region_3d for coordinate conversion
region = context.region
region_3d = context.space_data.region_3d
# Get bmesh from edit mode
bm = bmesh.from_edit_mesh(mesh)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
# Get mouse position in 3D space
# We'll project from the view to get a ray
view_vector = view3d_utils.region_2d_to_vector_3d(region, region_3d, self.mouse_pos)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, region_3d, self.mouse_pos)
print(f"\n=== DEBUG: Subdivide at Cursor ===")
print(f"Mouse position (2D): {self.mouse_pos}")
print(f"Ray origin (world): {ray_origin}")
print(f"View vector (world): {view_vector}")
# Convert to object local space
matrix_inv = obj.matrix_world.inverted()
ray_origin_local = matrix_inv @ ray_origin
view_vector_local = matrix_inv.to_3x3() @ view_vector
print(f"Ray origin (local): {ray_origin_local}")
print(f"View vector (local): {view_vector_local}")
# Find the average Z position of all vertices to determine the working plane
avg_z = sum(v.co.z for v in bm.verts) / len(bm.verts) if bm.verts else 0.0
print(f"Average mesh Z: {avg_z}")
# Intersect the ray with the working plane (Z = avg_z)
# Ray equation: P = ray_origin + t * view_vector
# Plane equation: Z = avg_z
# Solve for t: ray_origin.z + t * view_vector.z = avg_z
if abs(view_vector_local.z) < 1e-8:
# Ray is parallel to the plane, can't intersect
self.report({'WARNING'}, "View is parallel to mesh plane")
return {'CANCELLED'}
t_intersect = (avg_z - ray_origin_local.z) / view_vector_local.z
intersection_point = ray_origin_local + t_intersect * view_vector_local
print(f"Intersection point on mesh plane: {intersection_point}")
print(f" (should have Z ≈ {avg_z})")
# Find the edge closest to the intersection point
closest_edge = None
closest_distance = float('inf')
closest_t = 0.5
print(f"\nSearching through {len(bm.edges)} edges...")
for edge in bm.edges:
v1, v2 = edge.verts
edge_start = v1.co
edge_end = v2.co
# Find the point on the edge closest to the intersection point
t = self.find_closest_point_on_edge_to_point(
edge_start, edge_end, intersection_point
)
# Clamp t to [0, 1] to ensure we stay on the edge
t = max(0.0, min(1.0, t))
# Calculate the actual point on the edge
point_on_edge = edge_start + t * (edge_end - edge_start)
# Calculate 2D distance (ignoring Z) from this point to intersection
distance = (Vector((point_on_edge.x, point_on_edge.y)) -
Vector((intersection_point.x, intersection_point.y))).length
if distance < closest_distance:
closest_distance = distance
closest_edge = edge
closest_t = t
print(f"\nClosest edge found:")
print(f" Edge vertices: {closest_edge.verts[0].co} to {closest_edge.verts[1].co}")
print(f" 2D distance to intersection: {closest_distance}")
print(f" Parameter t: {closest_t}")
if not closest_edge:
self.report({'WARNING'}, "No edges found")
return {'CANCELLED'}
# Subdivide the closest edge
new_edge_data = bmesh.ops.subdivide_edges(
bm,
edges=[closest_edge],
cuts=1
)
# Find the new vertex
new_vert = None
for elem in new_edge_data['geom_inner']:
if isinstance(elem, bmesh.types.BMVert):
new_vert = elem
break
new_verts = []
if new_vert:
# Place the new vertex at the exact intersection point (cursor position)
new_position = intersection_point.copy()
print(f"\nPositioning new vertex:")
print(f" Target position (cursor): {new_position}")
print(f" Before assignment: {new_vert.co}")
new_vert.co = new_position
print(f" After assignment: {new_vert.co}")
# Transform to world space to see where it actually is
world_pos = obj.matrix_world @ new_vert.co
print(f" World position: {world_pos}")
# Show how close we are to the intersection point (should be 0)
distance_to_intersection = (Vector((new_vert.co.x, new_vert.co.y)) -
Vector((intersection_point.x, intersection_point.y))).length
print(f" 2D distance from cursor intersection: {distance_to_intersection}")
new_verts.append(new_vert)
# Deselect all geometry
for v in bm.verts:
v.select = False
for e in bm.edges:
e.select = False
for f in bm.faces:
f.select = False
# Select only the new vertices
for vert in new_verts:
vert.select = True
# Ensure the selection is flushed
bm.select_flush_mode()
# Update the mesh
bmesh.update_edit_mesh(mesh)
print(f"\n=== Subdivision complete ===\n")
self.report({'INFO'}, "Subdivided nearest edge")
# Invoke the grab/move tool
bpy.ops.transform.translate('INVOKE_DEFAULT')
return {'FINISHED'}
def find_closest_point_on_edge_to_point(self, edge_start, edge_end, point):
"""
Find parameter t [0,1] for the point on edge closest to the given point.
"""
# Edge direction
edge_dir = edge_end - edge_start
edge_length_sq = edge_dir.length_squared
if edge_length_sq < 1e-8:
# Edge has no length, return midpoint
return 0.5
# Vector from edge start to point
to_point = point - edge_start
# Project onto edge direction
t = to_point.dot(edge_dir) / edge_length_sq
return t
def find_closest_point_on_edge_to_ray(self, edge_start, edge_end, ray_origin, ray_dir):
"""
Find parameter t [0,1] for the point on edge closest to the view ray.
Uses the closest point between two 3D lines approach.
"""
# Edge direction
edge_dir = edge_end - edge_start
# Vector from ray origin to edge start
w = edge_start - ray_origin
# Parameters for closest points on two lines
a = edge_dir.dot(edge_dir)
b = edge_dir.dot(ray_dir)
c = ray_dir.dot(ray_dir)
d = edge_dir.dot(w)
e = ray_dir.dot(w)
# Avoid division by zero
denom = a * c - b * b
if abs(denom) < 1e-8:
# Lines are parallel, use midpoint
return 0.5
# Parameter for closest point on edge
t = (b * e - c * d) / denom
return t
def distance_point_to_ray(self, point, ray_origin, ray_dir):
"""
Calculate the perpendicular distance from a point to a ray.
"""
# Vector from ray origin to point
w = point - ray_origin
# Project w onto ray direction
c1 = w.dot(ray_dir)
c2 = ray_dir.dot(ray_dir)
if c2 < 1e-8:
return w.length
# Point on ray closest to our point
b = c1 / c2
point_on_ray = ray_origin + b * ray_dir
# Distance from point to closest point on ray
return (point - point_on_ray).length
def menu_func(self, context):
self.layout.separator()
self.layout.operator(MESH_OT_subdivide_at_cursor.bl_idname)
def register():
bpy.utils.register_class(MESH_OT_subdivide_at_cursor)
bpy.types.VIEW3D_MT_edit_mesh_edges.append(menu_func)
def unregister():
bpy.utils.unregister_class(MESH_OT_subdivide_at_cursor)
bpy.types.VIEW3D_MT_edit_mesh_edges.remove(menu_func)
if __name__ == "__main__":
register()