maximize object surface area with a plane

I’m using blender for some scripting to take molecular ball-and-stick models and make them ready for 3D printing.

One of the main issues I’m encountering is getting the somewhat irregularly shaped molecules in the best orientation for printing. Ideally, I would maximize the surface area of the molecule with the build plate, therefore getting the maximum surface on a plane is what I’m looking to do.

I thought I might just sample random orientations, find the face with the lowest z position, and then calculate the average distance between this face and all other faces. I tried this, as well as finding all faces that are within some threshold distance from the lowest z-position and using calc_area() for all of those faces to find a maximum. The results in both cases are inconsistent.

Any thoughts on how I might achieve this.

Thanks.

This might be late night delirium but i would think the best way to solve this would be to compute the convex hull and then take the face of the hull with the greatest number of coplanar vertices. That should be the largest number of points of the model you can put on the build plate at one time. Or maybe you want the face of the hull with the largest area or the one whose oriented bounding box has the greatest area since that would be the most stable side. Ignore me if this sounds like nonsense

It makes complete sense, but I’ll see how it works. The only potential drawback the comes to mind is that the convex hull might favor fewer points that are in-plane but further away from each other, as the surface between them would be larger. It would certainly be much faster than sampling random orientations!

I’ve rigged something that seems to work reasonably well using the convex_hull approach. Interestingly, the results are generally very similar to what I came up with looking at a series of random rotations. However, it is much much faster. I’ve included my script, for reference.

import bpy
import bmesh
from mathutils import Matrix, Vector


def bmesh_copy_from_object(obj, transform=True, triangulate=True, apply_modifiers=False):
    """
    Returns a transformed, triangulated copy of the mesh
    """


    assert(obj.type == 'MESH')


    if apply_modifiers and obj.modifiers:
        me = obj.to_mesh(bpy.context.scene, True, 'PREVIEW', calc_tessface=False)
        bm = bmesh.new()
        bm.from_mesh(me)
        bpy.data.meshes.remove(me)
    else:
        me = obj.data
        if obj.mode == 'EDIT':
            bm_orig = bmesh.from_edit_mesh(me)
            bm = bm_orig.copy()
        else:
            bm = bmesh.new()
            bm.from_mesh(me)


    # Remove custom data layers to save memory
    for elem in (bm.faces, bm.edges, bm.verts, bm.loops):
        for layers_name in dir(elem.layers):
            if not layers_name.startswith("_"):
                layers = getattr(elem.layers, layers_name)
                for layer_name, layer in layers.items():
                    layers.remove(layer)


    if transform:
        bm.transform(obj.matrix_world)


    if triangulate:
        bmesh.ops.triangulate(bm, faces=bm.faces)


    return bm




def getlargestface(obj):
    
    bm1 = bmesh_copy_from_object(obj)
    bmesh.ops.convex_hull(bm1,input=(bm1.verts),use_existing_faces=False)
    bm1.faces.ensure_lookup_table()
  
    largestface = 0
    largestidx = None
    faceidx = 0 
    for face in bm1.faces:
        facearea = face.calc_area()
        if facearea > largestface:
            largestidx = faceidx
            largestface = facearea
        faceidx += 1
    
    bm1.faces[largestidx].select = True
    facenorm = bm1.faces[largestidx].normal
    return (facenorm)


#Just for selected objects now    
obj = bpy.context.selected_objects[0]
#get normal of face with largest area
facenormal = getlargestface(obj)
#rotate object, from previous script
matrix_orig = obj.matrix_world.copy()
axis_src = matrix_orig.to_3x3() * facenormal
axis_dst = Vector((0, 0, -1))
matrix_rotate = matrix_orig.to_3x3()
matrix_rotate = matrix_rotate * axis_src.rotation_difference(axis_dst).to_matrix()
matrix_translation = Matrix.Translation(matrix_orig.to_translation())


obj.matrix_world = matrix_translation * matrix_rotate.to_4x4()

Cool! Thanks for sharing the script. Is this solving your issue or does it still give results that are obviously non-optimal?

It works more consistently than my other method and is much faster. There are still some scenarios where the convex hull is generated in such a way that the most intuitive face to have down is not the one that is selected. Here is such a case:



This is the object on the left and its convex hull on the right. The largest face is selected.

For printing purposes, it would make more sense to get maximum contact by putting the phenol ring face down, so this face:

But as you can see it has been split up into four faces (presumably because the sphere at the ‘top’ is slightly smaller than the others).

One way I could approach this is to group face normals from the convex hull that are within some threshold of each other and calculate the area of everything within the group. For now, however, this works well enough.

Actually, I might have found a less intensive solution than grouping all the face normals. I just threw in a limited dissolve:

bmesh.ops.dissolve_limit(bm1,angle_limit=0.09,verts=bm1.verts,edges=bm1.edges)

and it seems to work well (at least in this case). More testing to come.