1
$\begingroup$

For Blender python scripting, I understand how to, for the "poll" function of an operator, identify whether a selected object is mesh. You could do

context.object.type=='MESH'

Still, how could you create a boolean statement to determine whether a selected mesh object (if it is known that it is mesh) is a cube?

Thanks.

$\endgroup$
  • 1
    $\begingroup$ unfortunately, this can become a rather vast topic. for instance: do you consider an object a cube if the user had rotated it arbitrarily, and applied transforms after that? is a cube looking object, where some faces are not merged, a cube? what if normals are messed up? What I try to say is, please provide the rules on what needs to be fulfilled to have True as an answer of the function $\endgroup$ – aliasguru Jul 20 '16 at 18:27
  • $\begingroup$ What would need to be fulfilled to have true as the answer? It would need to have been originally created as a cube object, as opposed to a plane, icosphere, UV sphere, grid, torus, etc. Thanks $\endgroup$ – George Bentley Jul 20 '16 at 18:42
  • $\begingroup$ I added an answer based on a mesh being a cube, not sure how one would go about determining if a mesh was originally created as a cube tho? $\endgroup$ – batFINGER Jul 21 '16 at 14:11
  • $\begingroup$ Maybe if context.object.name.startswith("Cube") $\endgroup$ – batFINGER Jul 21 '16 at 20:01
2
$\begingroup$

One way to go about it, using answer from https://blender.stackexchange.com/a/53976/15543.

A cube has six sides, with orthogonal set of normals, all with equal area.

Below is code to split all faces into "sides" with same face normal and calculates the sides area and volume. The volume should be zero, if not the faces aren't coplanar and don't make one side of a cube.

Run the test code in edit mode.

If end up with 6 sides, equal area, all coplanar with orthogonal (either dot product is zero or sum length is zero ( v == -v) )

import bpy
import bmesh
from math import radians
from mathutils import Vector

context = bpy.context

def coplanar(faces, TOL=0.00001):
    bm2 = bmesh.new()
    verts = [v.co for f in faces for v in f.verts]
    o = sum(verts, Vector((0,0,0))) / len(verts) 
    for v in verts:
        # origin to center (matters for convex_hull)
        bm2.verts.new(v - o)
    #calculate a convex hull
    bmesh.ops.convex_hull(bm2, input=bm2.verts)
    # if the convex hull has no volume its on same plane.
    volume = bm2.calc_volume()
    bm2.free()

    return volume

# create a bmesh
TOL = 0.01
bm = bmesh.from_edit_mesh(context.edit_object.data)
# tried (unsuccesfully) to calculate orthonormal set from face normal
f = bm.faces[0]
normal = f.normal
v = Vector((1, 0, 0))
if normal.dot(v) < 0.000001: # parallel
    v = Vector((0, 1, 0))
#get the axis
x = normal
y = x.cross(v).normalized()
z = x.cross(y).normalized()
print("-" * 72)
faces = bm.faces[:]
sides = []
while len(faces):
    f = faces[0]
    axis = f.normal
    # has to be at least one to match f.normal
    side_faces = [f for f in faces if f.normal.angle(axis) < TOL]    
    area = sum(f.calc_area() for f in side_faces)
    volume = coplanar(side_faces)
    for f in side_faces:
        faces.remove(f)
    sides.append((axis, area, volume))
    print("Axis:", axis, "Area %5.2f Volume %5.2f" % (area, volume))

Won't be a cube if len(sides) != 6 and so on.

Better if an orthogonal axis set was calculated from the first face normal, but couldn't work that out.

$\endgroup$
  • $\begingroup$ some nice hints in this answer. If we combine ours we'd make a watertight solution :) My answer still lacks in determining square faces, but if I use your approach to calculate the face area, and all six faces have the same face area, the proof is made. Checking the normals would not be necessary then. $\endgroup$ – aliasguru Jul 21 '16 at 19:37
  • $\begingroup$ Maybe the mesh is manifold $\endgroup$ – batFINGER Jul 21 '16 at 19:54
0
$\begingroup$

The following script tests two things:

  1. a cube will have exactly 8 vertices, 6 faces and 12 edges (simpleCompare function)
  2. the lengths of all edges has to be the same within a tolerance (edgeLengthCompare function)

What it does not test yet is if all angles are 90 degrees. Theoretically you could create a sheared object (rhomboid) which fulfills the same conditions. It would be sufficient to measure one diagonal through the cube (or rhomboid) and calculate if its length is feasible. If the object is sheared, it won't. However, this calculation, if done in a simple way, would assume a certain topology on your object. As long as the cubes are really Blender default cubes, one could do this. Otherwise, improvisation is needed because vertex orders could have changed.

Tolerances are needed because the user could have rotated the mesh and applied transforms, that inevitably creates some minor edge length differences because of rounding errors.

Start with selecting all objects you would like to test, then run the script. If one of the two conditions is not met for the selected objects, the script will print an error message to the console, and deselect the object in the viewport. So after execution, you'll have only cubes selected.

Here's the code:

import bpy, math

def edgeLengthCompare(obData, tolerance):
    initLength = None

    for edge in obData.edges:
        # two verts have a distance
        v0 = obData.vertices[edge.vertices[0]]
        v1 = obData.vertices[edge.vertices[1]]
        edgeLength = (v0.co - v1.co).length
        print (edgeLength, initLength)

        if initLength != None:
            if not math.isclose(edgeLength, initLength, abs_tol=tolerance):
                return False 
        else:
            initLength = edgeLength

    return True

def simpleCompare(obData):
    return True if len(obData.vertices) == 8 and len(obData.edges)==12 and len(obData.polygons)==6 else False


# set a compare tolerance, needed for cubes which were rotated and then have transforms applied
tolerance = 0.0001


for obj in bpy.context.selected_editable_objects:

    if obj.type != 'MESH': continue

    # get the object Data of the current object
    obData = obj.data
    print('============= TESTING OBJECT %s ==============' % obj.name)

    # if all edges have equal length, object is officially a cube
    if simpleCompare(obData) and edgeLengthCompare(obData, tolerance):
        print ('Object %s is a Cube' % obj.name)
    else:
        print ('Cube test failed, deselecting')
        obj.select = False
$\endgroup$
  • $\begingroup$ What if the cube is subdivided or faces are tris? $\endgroup$ – batFINGER Jul 21 '16 at 19:35
  • $\begingroup$ True. For the subdivided case, one could simply reject objects with modifiers on them. Which is also not totally clean, because some alter the mesh, some don't. But it starts to become ridiculous to check for EVERY case, and since George Bentley didn't include this in his clarifying comment I didn't consider it. For the faces as tris case: That would not match the default cube anyways, so it could be skipped from my point of view. $\endgroup$ – aliasguru Jul 21 '16 at 19:41
  • $\begingroup$ You can apply a decimate modifier to a copy of the object before checking. $\endgroup$ – qwazix Jan 4 '17 at 15:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.