278 lines
No EOL
9.4 KiB
Python
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() |