"""
Remeshing Handlers
Business logic for remeshing operations that run inside Blender.
"""
import bpy
import traceback
def decimate(ratio=0.5, mode="COLLAPSE", angle_limit=5.0):
"""Apply decimation to the active mesh object"""
try:
active_obj = bpy.context.active_object
if not active_obj or active_obj.type != 'MESH':
return {"error": "No active mesh object selected"}
# Validate mode - Blender API uses "UNSUBDIV" not "UNSUBDIVIDE"
# Accept both forms for user convenience
mode_map = {"UNSUBDIVIDE": "UNSUBDIV"}
mode = mode_map.get(mode, mode)
valid_modes = ["COLLAPSE", "DISSOLVE", "UNSUBDIV"]
if mode not in valid_modes:
return {"error": f"Invalid mode: {mode}. Must be one of: COLLAPSE, DISSOLVE, UNSUBDIV (or UNSUBDIVIDE)"}
# Store old face count
old_faces = len(active_obj.data.polygons)
# Add DECIMATE modifier
modifier = active_obj.modifiers.new(name="Decimate", type='DECIMATE')
modifier.decimate_type = mode
if mode == "COLLAPSE":
modifier.ratio = ratio
elif mode == "DISSOLVE":
modifier.angle_limit = angle_limit * (3.14159 / 180.0) # Convert to radians
elif mode == "UNSUBDIV":
modifier.iterations = int(ratio * 10) # Use ratio as iteration count (scaled)
# Apply the modifier
bpy.context.view_layer.objects.active = active_obj
bpy.ops.object.modifier_apply(modifier=modifier.name)
# Get new face count
new_faces = len(active_obj.data.polygons)
# Calculate actual ratio achieved
ratio_achieved = new_faces / old_faces if old_faces > 0 else 0.0
return {
"success": True,
"old_faces": old_faces,
"new_faces": new_faces,
"ratio_achieved": ratio_achieved
}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to apply decimate: {str(e)}"}
def shrinkwrap_reproject(high, low, method="NEAREST_SURFACEPOINT", offset=0.0):
"""Apply shrinkwrap modifier to reproject low-poly mesh onto high-poly mesh"""
try:
# Validate method
valid_methods = ["NEAREST_SURFACEPOINT", "PROJECT", "NEAREST_VERTEX", "TARGET_PROJECT"]
if method not in valid_methods:
return {"error": f"Invalid method: {method}. Must be one of: {', '.join(valid_methods)}"}
# Get objects
high_obj = bpy.data.objects.get(high)
low_obj = bpy.data.objects.get(low)
# Precondition checks
if not high_obj:
return {"error": f"High-poly object not found: {high}"}
if not low_obj:
return {"error": f"Low-poly object not found: {low}"}
if high_obj.type != 'MESH':
return {"error": f"High-poly object is not a mesh: {high}"}
if low_obj.type != 'MESH':
return {"error": f"Low-poly object is not a mesh: {low}"}
# Add SHRINKWRAP modifier to low-poly object
modifier = low_obj.modifiers.new(name="Shrinkwrap", type='SHRINKWRAP')
modifier.target = high_obj
modifier.wrap_method = method
modifier.offset = offset
# Apply the modifier
bpy.context.view_layer.objects.active = low_obj
bpy.ops.object.modifier_apply(modifier=modifier.name)
return {
"success": True,
"low_poly": low,
"high_poly": high,
"method": method
}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to apply shrinkwrap: {str(e)}"}
def remesh_voxel(voxel_size=0.1, adaptivity=0.0, preserve_volume=False):
"""Apply voxel remesh to the active mesh object"""
try:
active_obj = bpy.context.active_object
if not active_obj or active_obj.type != 'MESH':
return {"error": "No active mesh object selected"}
# Store old face count
old_faces = len(active_obj.data.polygons)
# Add REMESH modifier
modifier = active_obj.modifiers.new(name="Voxel Remesh", type='REMESH')
modifier.mode = 'VOXEL'
modifier.voxel_size = voxel_size
modifier.adaptivity = adaptivity
# preserve_volume is not a direct property of Voxel Remesh modifier in standard API,
# but 'use_smooth_shade' is common. Voxel remesh is volumetric.
# Let's check if preserve_volume maps to something else or is just ignored for voxel.
# Actually, the operator `bpy.ops.object.voxel_remesh()` exists but modifiers are non-destructive.
# The tool description implies an operator-like effect.
# Let's use the modifier for consistency with other tools if possible,
# but voxel remesh is often done via `bpy.ops.object.voxel_remesh()`.
# However, `bpy.ops` can be flaky in background. Modifier is safer.
# Apply the modifier
bpy.context.view_layer.objects.active = active_obj
bpy.ops.object.modifier_apply(modifier=modifier.name)
# Get new face count
new_faces = len(active_obj.data.polygons)
return {
"success": True,
"old_faces": old_faces,
"new_faces": new_faces,
"voxel_size": voxel_size
}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to apply voxel remesh: {str(e)}"}
def quadriflow_remesh(target_faces=5000, preserve_sharp=False, use_symmetry=False):
"""Apply QuadriFlow remesh to the active mesh object"""
try:
active_obj = bpy.context.active_object
if not active_obj or active_obj.type != 'MESH':
return {"error": "No active mesh object selected"}
# QuadriFlow is an operator, not a modifier.
# bpy.ops.object.quadriflow_remesh()
# Store old face count
old_faces = len(active_obj.data.polygons)
# Ensure mode is OBJECT
if bpy.context.object.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
# Run QuadriFlow
# Note: This might be slow and blocking.
bpy.ops.object.quadriflow_remesh(
target_faces=target_faces,
use_mesh_symmetry=use_symmetry,
use_preserve_sharp=preserve_sharp
)
# Get new face count
new_faces = len(active_obj.data.polygons)
return {
"success": True,
"old_faces": old_faces,
"new_faces": new_faces,
"target_faces": target_faces
}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to apply QuadriFlow: {str(e)}"}