Blender add on 3d print mesh Fixer.

Image for Blender add on 3d print mesh Fixer.
# Blender Addon: 3D Print Mesh Fixer. save as (plain text) meshfix.py 

#  install add on in blender edit > preferences > addons.  

# This script creates a panel in the 3D View's Sidebar (N-Panel)

# with basic and advanced tools to prepare meshes for 3D printing.

bl_info = {

    "name": "3D Print Mesh Fixer",

    "author": "Peter Farell",

    "version": (1, 5, 0),

    "blender": (2, 83, 0), # Version for Remesh modifier

    "location": "View3D > Sidebar > BruteForceMesh",

    "description": "Automates mesh fixes and provides targeted tools for stubborn errors.",

    "warning": "The Aggressive Fix (Remesh) will alter topology and can be slow on high-poly meshes.",

    "doc_url": "",

    "category": "Mesh",

}

import bpy

import bmesh

# --- UTILITY FUNCTIONS ---

def get_non_manifold_count(obj):

    """Efficiently counts non-manifold vertices on an object using bmesh."""

    if obj.mode != 'OBJECT':

        bpy.ops.object.mode_set(mode='OBJECT')

    bm = bmesh.new()

    bm.from_mesh(obj.data)

    bm.edges.ensure_lookup_table()

    non_manifold_edges = [e for e in bm.edges if not e.is_manifold]

    bm.free()

    return len(non_manifold_edges)

def create_backup(context, obj, self_operator):

    """Creates a hidden backup of the object if it's below a polygon threshold."""

    poly_threshold = 250000

    poly_count = len(obj.data.polygons)

    if poly_count < poly_threshold:

        try:

            backup_obj = obj.copy()

            backup_obj.data = obj.data.copy()

            backup_obj.name = obj.name + "_backup"

            context.collection.objects.link(backup_obj)

            backup_obj.hide_set(True)

            backup_obj.hide_render = True

            self_operator.report({'INFO'}, f"Created backup object: {backup_obj.name}")

        except Exception as e:

            self_operator.report({'ERROR'}, f"Could not create backup: {e}")

    else:

        self_operator.report({'WARNING'}, f"Mesh has {poly_count} polys. Skipping backup to save memory.")

# --- MAIN OPERATOR ---

class MESH_OT_fix_for_3d_print_standard(bpy.types.Operator):

    """(Gentle) Automates fixing common mesh problems with a feedback loop"""

    bl_idname = "mesh.fix_for_3d_print_standard"

    bl_label = "Fix Mesh"

    bl_options = {'REGISTER', 'UNDO'}

    # Properties for toggling repair steps

    do_merge: bpy.props.BoolProperty(name="Merge by Distance", default=True)

    do_normals: bpy.props.BoolProperty(name="Recalculate Normals", default=True)

    do_loose: bpy.props.BoolProperty(name="Delete Loose Geometry", default=True)

    do_holes: bpy.props.BoolProperty(name="Fill Holes", default=True)

    do_dissolve: bpy.props.BoolProperty(name="Limited Dissolve", default=True, description="Cleans up flat surfaces after other operations")

    do_triangulate: bpy.props.BoolProperty(name="Triangulate", default=True)

    

    max_iterations: bpy.props.IntProperty(

        name="Max Iterations",

        description="Number of times to repeat the fix process if errors persist.",

        default=3, min=1, max=10

    )

    @classmethod

    def poll(cls, context):

        return context.active_object is not None and context.active_object.type == 'MESH'

    def draw(self, context):

        layout = self.layout

        layout.prop(self, "max_iterations")

        layout.separator()

        layout.prop(self, "do_merge")

        layout.prop(self, "do_normals")

        layout.prop(self, "do_loose")

        layout.prop(self, "do_holes")

        layout.prop(self, "do_dissolve")

        layout.prop(self, "do_triangulate")

    def execute(self, context):

        obj = context.active_object

        original_mode = obj.mode

        if original_mode != 'OBJECT':

            bpy.ops.object.mode_set(mode='OBJECT')

        create_backup(context, obj, self)

        

        self.report({'INFO'}, "Starting standard mesh fix process...")

        last_error_count = float('inf')

        

        initial_error_count = get_non_manifold_count(obj)

        if initial_error_count == 0:

            self.report({'INFO'}, "No non-manifold errors found. Nothing to do.")

            return {'FINISHED'}

        for i in range(self.max_iterations):

            self.report({'INFO'}, f"--- Starting Repair Loop {i+1}/{self.max_iterations} ---")

            

            # --- Perform all operations within a single bmesh block for stability and speed ---

            bm = bmesh.new()

            bm.from_mesh(obj.data)

            if self.do_merge:

                bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)

            if self.do_normals:

                bmesh.ops.recalc_face_normals(bm, faces=bm.faces)

            if self.do_loose:

                loose_verts = [v for v in bm.verts if not v.link_edges]

                if loose_verts:

                    bmesh.ops.delete(bm, geom=loose_verts, context='VERTS')

            if self.do_holes:

                bmesh.ops.holes_fill(bm, edges=[e for e in bm.edges if e.is_boundary])

            if self.do_dissolve:

                bmesh.ops.dissolve_limit(bm, angle_limit=0.0872665, verts=bm.verts, edges=bm.edges)

            if self.do_triangulate:

                bmesh.ops.triangulate(bm, faces=bm.faces[:])

            # Update the mesh and free the bmesh

            bm.to_mesh(obj.data)

            bm.free()

            obj.data.update()

            current_error_count = get_non_manifold_count(obj)

            

            if current_error_count == 0:

                self.report({'INFO'}, f"SUCCESS: Mesh fixed after {i+1} loop(s).")

                break

            if current_error_count >= last_error_count:

                self.report({'WARNING'}, f"No improvement after loop {i+1}. Stopping.")

                break

            last_error_count = current_error_count

            self.report({'INFO'}, f"Loop {i+1} finished. {current_error_count} errors remain. Continuing...")

        

        if current_error_count > 0:

            self.report({'WARNING'}, f"WARNING: {current_error_count} non-manifold elements remain.")

            bpy.ops.object.mode_set(mode='EDIT')

            bpy.ops.mesh.select_all(action='DESELECT')

            bpy.ops.mesh.select_non_manifold()

        else:

            bpy.ops.object.mode_set(mode=original_mode)

            

        return {'FINISHED'}

    def invoke(self, context, event):

        return context.window_manager.invoke_props_dialog(self)

# --- TARGETED FIX OPERATORS ---

class MESH_OT_reselect_problems(bpy.types.Operator):

    """Selects non-manifold geometry"""

    bl_idname = "mesh.reselect_problems"

    bl_label = "Re-Select Problem Areas"

    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):

        if context.object.mode != 'EDIT':

            bpy.ops.object.mode_set(mode='EDIT')

        bpy.ops.mesh.select_all(action='DESELECT')

        bpy.ops.mesh.select_non_manifold()

        self.report({'INFO'}, "Selected non-manifold geometry.")

        return {'FINISHED'}

class MESH_OT_close_gaps(bpy.types.Operator):

    """Attempts to close small gaps by displacing and merging"""

    bl_idname = "mesh.close_small_gaps"

    bl_label = "Close Small Gaps"

    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):

        obj = context.active_object

        self.report({'INFO'}, "Attempting to close small gaps...")

        bpy.ops.object.mode_set(mode='EDIT')

        bpy.ops.mesh.select_all(action='SELECT')

        # Displace outwards slightly

        bpy.ops.transform.shrink_fatten(value=0.0001)

        # Merge the now-overlapping vertices

        bpy.ops.mesh.remove_doubles(threshold=0.0002)

        bpy.ops.object.mode_set(mode='OBJECT')

        self.report({'INFO'}, "Gap closing attempt finished. Check for remaining errors.")

        return {'FINISHED'}

class MESH_OT_rebuild_shell(bpy.types.Operator):

    """Rebuilds the outer shell using a Shrinkwrap modifier"""

    bl_idname = "mesh.rebuild_shell"

    bl_label = "Rebuild Outer Shell"

    bl_options = {'REGISTER', 'UNDO'}

    thickness: bpy.props.FloatProperty(name="Thickness", default=0.001, min=0.0, unit='LENGTH')

    offset: bpy.props.FloatProperty(name="Offset", default=0.0, unit='LENGTH')

    def execute(self, context):

        target_obj = context.active_object

        self.report({'INFO'}, "Rebuilding outer shell...")

        

        # Create a new mesh object (a simple cube is fine)

        bpy.ops.mesh.primitive_cube_add(enter_editmode=False, align='WORLD', location=target_obj.location)

        new_obj = context.active_object

        new_obj.name = target_obj.name + "_shell"

        

        # Add a subdivision surface modifier for a smoother base

        subdiv = new_obj.modifiers.new(name="Subdiv", type='SUBSURF')

        subdiv.levels = 3

        

        # Add Shrinkwrap modifier

        shrinkwrap = new_obj.modifiers.new(name="Shrinkwrap", type='SHRINKWRAP')

        shrinkwrap.target = target_obj

        

        # Add Solidify modifier

        solidify = new_obj.modifiers.new(name="Solidify", type='SOLIDIFY')

        solidify.thickness = self.thickness

        solidify.offset = self.offset

        

        # Apply all modifiers

        bpy.ops.object.modifier_apply(modifier=subdiv.name)

        bpy.ops.object.modifier_apply(modifier=shrinkwrap.name)

        bpy.ops.object.modifier_apply(modifier=solidify.name)

        

        self.report({'INFO'}, f"Created new manifold shell object: {new_obj.name}")

        return {'FINISHED'}

        

    def invoke(self, context, event):

        return context.window_manager.invoke_props_dialog(self)

# --- AGGRESSIVE FIX OPERATOR ---

class MESH_OT_fix_for_3d_print_aggressive(bpy.types.Operator):

    """(Aggressive) Rebuilds the mesh using Voxel Remesh"""

    bl_idname = "mesh.fix_for_3d_print_aggressive"

    bl_label = "Aggressive Fix (Remesh)"

    bl_options = {'REGISTER', 'UNDO'}

    # ... (rest of the class is unchanged) ...

    voxel_size: bpy.props.FloatProperty(

        name="Voxel Size",

        description="Controls detail. Smaller is more detailed but slower.",

        default=0.01, min=0.001, max=1.0, unit='LENGTH'

    )

    @classmethod

    def poll(cls, context):

        return context.active_object is not None and context.active_object.type == 'MESH'

    def execute(self, context):

        obj = context.active_object

        if obj.mode != 'OBJECT':

            bpy.ops.object.mode_set(mode='OBJECT')

        create_backup(context, obj, self)

        self.report({'INFO'}, f"Starting aggressive remesh with voxel size: {self.voxel_size:.4f}")

        remesh_mod = obj.modifiers.new(name="PrintFixRemesh", type='REMESH')

        remesh_mod.mode = 'VOXEL'

        remesh_mod.voxel_size = self.voxel_size

        remesh_mod.use_smooth_shade = True

        bpy.ops.object.modifier_apply(modifier=remesh_mod.name)

        self.report({'INFO'}, "Remesh complete. Running a standard cleanup pass...")

        bpy.ops.mesh.fix_for_3d_print_standard('INVOKE_DEFAULT', do_dissolve=False, max_iterations=1)

        return {'FINISHED'}

    def invoke(self, context, event):

        return context.window_manager.invoke_props_dialog(self)

# --- UI PANELS ---

class VIEW3D_PT_mesh_fixer_panel(bpy.types.Panel):

    """Creates a Panel in the 3D View Sidebar"""

    bl_label = "3D Print Fixer"

    bl_idname = "VIEW3D_PT_mesh_fixer"

    bl_space_type = 'VIEW_3D'

    bl_region_type = 'UI'

    bl_category = '3D Print'

    def draw(self, context):

        layout = self.layout

        box = layout.box()

        box.label(text="Standard Repair", icon='TOOL_SETTINGS')

        row = box.row()

        row.scale_y = 1.5

        row.operator(MESH_OT_fix_for_3d_print_standard.bl_idname, text="Fix Mesh...")

        box.label(text="Gentle fixes with a feedback loop.", icon='INFO')

        layout.separator()

        box = layout.box()

        box.label(text="Aggressive Repair", icon='MOD_REMESH')

        row = box.row()

        row.scale_y = 1.5

        row.operator(MESH_OT_fix_for_3d_print_aggressive.bl_idname, text="Aggressive Fix...")

        box.label(text="Rebuilds mesh to fix intersections.", icon='ERROR')

        box.label(text="May lose fine details.", icon='ERROR')

        layout.separator()

        info_box = layout.box()

        info_box.label(text="Note: A hidden backup is made for", icon='LIGHT')

        info_box.label(text="meshes under 250k polygons.")

class VIEW3D_PT_targeted_fix_panel(bpy.types.Panel):

    """Shows contextual operators when non-manifold errors are found"""

    bl_label = "Targeted Fixes"

    bl_idname = "VIEW3D_PT_targeted_fixer"

    bl_space_type = 'VIEW_3D'

    bl_region_type = 'UI'

    bl_category = '3D Print'

    bl_options = {'DEFAULT_CLOSED'}

    @classmethod

    def poll(cls, context):

        obj = context.active_object

        # Only show this panel if there is an active mesh object and it has selected vertices (implying errors are selected)

        return (obj is not None and 

                obj.type == 'MESH' and 

                obj.data.total_vert_sel > 0 and

                context.mode == 'EDIT_MESH')

    def draw(self, context):

        layout = self.layout

        layout.label(text="Errors remain. Try these tools:", icon='ERROR')

        

        box = layout.box()

        box.label(text="Close Tiny Holes", icon='MOD_WAVE')

        box.operator(MESH_OT_close_gaps.bl_idname)

        

        box = layout.box()

        box.label(text="Fix Internal Geometry", icon='MOD_SHRINKWRAP')

        box.operator(MESH_OT_rebuild_shell.bl_idname, text="Rebuild Outer Shell...")

        layout.separator()

        layout.operator(MESH_OT_reselect_problems.bl_idname)

# --- REGISTRATION ---

classes = (

    MESH_OT_fix_for_3d_print_standard,

    MESH_OT_fix_for_3d_print_aggressive,

    MESH_OT_reselect_problems,

    MESH_OT_close_gaps,

    MESH_OT_rebuild_shell,

    VIEW3D_PT_mesh_fixer_panel,

    VIEW3D_PT_targeted_fix_panel,

)

def register():

    for cls in classes:

        bpy.utils.register_class(cls)

def unregister():

    for cls in reversed(classes):

        bpy.utils.unregister_class(cls)

if __name__ == "__main__":

    register()