"""
Retopology Workflow Handlers
Functions for setting up proper retopology workflow in Blender.
Creates correct object relationships instead of modifying the highpoly source.
"""
import bpy
import bmesh
import traceback
def create_retopo_setup(
source_object,
retopo_name=None,
initial_mesh="PLANE"
):
"""
Create a proper retopology setup with shrinkwrap and snapping configured.
Creates a new low-poly object for retopology work, leaving the high-poly
source untouched. Configures shrinkwrap modifier and snapping settings.
Parameters:
- source_object: Name of the high-poly source mesh to retopologize
- retopo_name: Name for the new retopo object (default: source_object + "_retopo")
- initial_mesh: Starting mesh type - "PLANE", "CUBE", "SPHERE", "EMPTY" (default: "PLANE")
Returns the name of the created retopo object.
"""
try:
# Get source object
source = bpy.data.objects.get(source_object)
if not source:
return {"error": f"Source object '{source_object}' not found"}
if source.type != 'MESH':
return {"error": "Source object must be a mesh"}
# Generate retopo name
if not retopo_name:
retopo_name = f"{source_object}_retopo"
# Check if retopo object already exists
if bpy.data.objects.get(retopo_name):
return {"error": f"Object '{retopo_name}' already exists. Choose a different name."}
# Create initial mesh
if initial_mesh == "PLANE":
bpy.ops.mesh.primitive_plane_add(size=1, location=source.location)
elif initial_mesh == "CUBE":
bpy.ops.mesh.primitive_cube_add(size=1, location=source.location)
elif initial_mesh == "SPHERE":
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.5, location=source.location)
elif initial_mesh == "EMPTY":
# Create empty mesh
mesh = bpy.data.meshes.new(retopo_name)
retopo_obj = bpy.data.objects.new(retopo_name, mesh)
bpy.context.collection.objects.link(retopo_obj)
bpy.context.view_layer.objects.active = retopo_obj
else:
return {"error": f"Unknown initial_mesh type: {initial_mesh}"}
# Get the created object
retopo_obj = bpy.context.active_object
retopo_obj.name = retopo_name
# Scale to approximate source size
source_dims = source.dimensions
if initial_mesh != "EMPTY":
max_dim = max(source_dims)
if max_dim > 0:
retopo_obj.dimensions = source_dims
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
# Add Shrinkwrap modifier
shrinkwrap = retopo_obj.modifiers.new(name="Shrinkwrap_Retopo", type='SHRINKWRAP')
shrinkwrap.target = source
shrinkwrap.wrap_method = 'NEAREST_SURFACEPOINT'
shrinkwrap.wrap_mode = 'ON_SURFACE'
shrinkwrap.show_on_cage = True # Show in Edit Mode
# Configure snapping for retopology
ts = bpy.context.scene.tool_settings
ts.use_snap = True
ts.snap_target = 'CLOSEST'
# Blender 4.0+ uses FACE_PROJECT instead of FACE + use_snap_project
if hasattr(ts, 'use_snap_project'):
# Blender 3.x API
ts.snap_elements = {'FACE'}
ts.use_snap_project = True
else:
# Blender 4.x API - FACE_PROJECT includes projection
ts.snap_elements = {'FACE_PROJECT'}
# Backface culling - check attribute exists for compatibility
if hasattr(ts, 'use_snap_backface_culling'):
ts.use_snap_backface_culling = True
# Enable X-Ray for visibility
retopo_obj.show_in_front = True
# Set source to wireframe display for better visibility
source.display_type = 'WIRE'
# Select retopo object and make it active
bpy.ops.object.select_all(action='DESELECT')
retopo_obj.select_set(True)
bpy.context.view_layer.objects.active = retopo_obj
return {
"success": True,
"retopo_object": retopo_name,
"source_object": source_object,
"initial_mesh": initial_mesh,
"settings_applied": [
"Shrinkwrap modifier added",
"Face snapping enabled",
"X-Ray enabled on retopo object",
"Source set to wireframe display"
]
}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to create retopo setup: {str(e)}"}
def project_vertices(object_name, target_object, method="NEAREST_SURFACEPOINT"):
"""
Project vertices of an object onto target surface.
Parameters:
- object_name: Name of object whose vertices to project
- target_object: Name of target surface object
- method: Projection method - "NEAREST_SURFACEPOINT", "PROJECT", "NEAREST_VERTEX"
Returns success status and number of vertices projected.
"""
try:
obj = bpy.data.objects.get(object_name)
target = bpy.data.objects.get(target_object)
if not obj:
return {"error": f"Object '{object_name}' not found"}
if not target:
return {"error": f"Target object '{target_object}' not found"}
if obj.type != 'MESH' or target.type != 'MESH':
return {"error": "Both objects must be meshes"}
# Store current mode
original_mode = obj.mode
# Switch to object mode
if original_mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
# Select and activate object
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
# Add temporary shrinkwrap modifier
shrinkwrap = obj.modifiers.new(name="TempShrinkwrap", type='SHRINKWRAP')
shrinkwrap.target = target
shrinkwrap.wrap_method = method
# Apply the modifier
bpy.ops.object.modifier_apply(modifier="TempShrinkwrap")
vert_count = len(obj.data.vertices)
# Restore mode
if original_mode != 'OBJECT':
bpy.ops.object.mode_set(mode=original_mode)
return {
"success": True,
"object": object_name,
"target": target_object,
"vertices_projected": vert_count,
"method": method
}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to project vertices: {str(e)}"}