2

Often for blender scripts have to calculate an encompassing bounding box from a collection of 3D points, for example sake the default blender cube bounding box as input,

coords = np.array(
     [[-1.  1. -1.],
      [-1.  1.  1.],
      [ 1. -1. -1.],
      [ 1. -1.  1.],
      [ 1.  1. -1.],
      [ 1.  1.  1.]]
 )

bfl = coords.min(axis=0)
tbr = coords.max(axis=0)

G  = np.array((bfl, tbr)).T
bbox_coords = [i for i in itertools.product(*G)]

The bounding box coords for example case will be the cube coords in same order

Looking for some python "iteration magic" using above and ("left", "right"), ("front", "back"),("top", "bottom") , to make a helper class

>>> bbox = BBox(bfl, tbr)
>>> bbox.bottom.front.left
(-1, -1, -1)

>>> bbox.top.front
(0, -1, 1)

>> bbox.bottom
(0, 0, -1)

ie a corner vertex, center of an edge, center of a rectangle. (the average sum of 1, 2, or 4 corners) In blender top is +Z and front is -Y.

Was originally looking at something like populating a nested dictionary with static calculated values

d = {
    "front" : {
        "co" : (0, -1, 0),
        "top" : {
            "co" : (0, -1, 1),
            "left" : {"co" : (-1, -1, 1)},
            }
        }   
    }

Object-like attribute access for nested dictionary

EDIT

To avoid posting an XY Problem, ie posting in question the way I've been approaching this, have added an answer below with where I was at with it. Apologies as I forgot to mention could instead choose north, south, east and west for x and y axis directions, and desire the ability to change.

Feel that looping over 8 corner verts is the way to go re making the "swizzle" dictionary with vertex index as leaf nodes. The vertex indices of "front" face or top bottom right corner don't change.

It's using this as a base for a class that is instanced with the coordinates or bfl, tbr is where no matter what I do I always feel there is a "better" way to go than what I am doing now.

batFINGER
  • 484
  • 2
  • 10

3 Answers3

1

Here are two similar versions. The idea of both is that you always return a BBox object and only alter a variable x which indicates which dimensions you have specified via left, right, ... Finally you have a function which uses x to calculate the center of the remaining corners.

The first approach uses functions so you have to call them bbox.bottom().front().left().c(). The main difference here is that not all the combinations

top
top left
top right
top left front
...

are computed when creating the object, but only when you call them.


import numpy as np
import itertools

class BBox:
    """
    ("left", "right"), -x, +x
    ("front", "back"), -y, +y
    ("bottom", "top"), -z, +z
    """
    def __init__(self, bfl, tbr):
        self.bfl = bfl
        self.tbr = tbr

        self.g = np.array((bfl, tbr)).T

        self.x = [[0, 1], [0, 1], [0, 1]]

    def c(self):  # get center coordinates
        return np.mean([i for i in itertools.product(*[self.g[i][self.x[i]] for i in range(3)])], axis=0)

    def part(self, i, xi):
        assert len(self.x[i]) == 2
        b2 = BBox(bfl=self.bfl, tbr=self.tbr)
        b2.x = self.x.copy()
        b2.x[i] = [xi]
        return b2

    def left(self):
        return self.part(i=0, xi=0)

    def right(self):
        return self.part(i=0, xi=1)

    def front(self):
        return self.part(i=1, xi=0)

    def back(self):
        return self.part(i=1, xi=1)

    def bottom(self):
        return self.part(i=2, xi=0)

    def top(self):
        return self.part(i=2, xi=1)


bbox = BBox(bfl=[-1, -1, -1], tbr=[1, 1, 1])
>>> bbox.bottom().front().left().c()
(-1, -1, -1)

>>> bbox.top().front().c()
(0, -1, 1)

>>> bbox.bottom().c()
(0, 0, -1)

The second approach uses attributes which are in itself BBox objects. When you uncomment the print statement in the init function you get an idea of all the recursive calls which are happening during construction. So while it might be more complicated to see what is going on here, you have more convenience when accessing the attributes.

class BBox:
    def __init__(self, bfl, tbr, x=None):
        self.bfl = bfl
        self.tbr = tbr
        self.g = np.array((bfl, tbr)).T

        self.x = [[0, 1], [0, 1], [0, 1]] if x is None else x
        
        # print(self.x)  # Debugging 
        self.left = self.part(i=0, xi=0)
        self.right = self.part(i=0, xi=1)
        self.front = self.part(i=1, xi=0)
        self.back = self.part(i=1, xi=1)
        self.bottom = self.part(i=2, xi=0)
        self.top = self.part(i=2, xi=1)

    def c(self):  # get center coordinates
        return np.mean([i for i in itertools.product(*[self.g[i][self.x[i]] 
                        for i in range(3)])], axis=0)

    def part(self, i, xi):
        if len(self.x[i]) < 2:
            return None
        x2 = self.x.copy()
        x2[i] = [xi]
        return BBox(bfl=self.bfl, tbr=self.tbr, x=x2)

bbox = BBox(bfl=[-1, -1, -1], tbr=[1, 1, 1])
>>> bbox.bottom.front.left.c()
(-1, -1, -1)

You could also add something like this at the end of the constructor, to remove the invalid attributes. (to prevent stuff like bbox.right.left.c()). They were None before but AttributeError might be more appropriate.

   def __init__(self, bfl, tbr, x=None):
       ...
       for name in ['left', 'right', 'front', 'back', 'bottom', 'top']:
           if getattr(self, name) is None:
               delattr(self, name)

And you could add a __repr__()method as well:

    def __repr__(self):
        return repr(self.get_vertices())

    def get_vertices(self):
        return [i for i in itertools.product(*[self.g[i][self.x[i]]
                                               for i in range(3)])]

    def c(self):  # get center coordinates
        return np.mean(self.get_vertices(), axis=0)


bbox.left.front
# [(-1, -1, -1), (-1, -1, 1)]
bbox.left.front.c()
# array([-1., -1.,  0.])

EDIT

After coming back to this after a while I think it is better to only add the relevant attributes and not add all and than delete half of them afterwards. So the most compact / convenient class I can come up with is:

class BBox:
    def __init__(self, bfl, tbr, x=None):
        self.bfl, self.tbr = bfl, tbr
        self.g = np.array((bfl, tbr)).T
        self.x = [[0, 1], [0, 1], [0, 1]] if x is None else x

        for j, name in enumerate(['left', 'right', 'front', 'back', 'bottom', 'top']):
            temp = self.part(i=j//2, xi=j%2)
            if temp is not None:
                setattr(self, name, temp)

    def c(self):  # get center coordinates
        return np.mean([x for x in itertools.product(*[self.g[i][self.x[i]]
                                                       for i in range(3)])], axis=0)

    def part(self, i, xi):
        if len(self.x[i]) == 2:
            x2, x2[i] = self.x.copy(), [xi]
            return BBox(bfl=self.bfl, tbr=self.tbr, x=x2)
scleronomic
  • 4,392
  • 1
  • 13
  • 43
  • Once again thankyou for your answers, apologies for not getting back sooner. Have added an answer (not to accept) to demo where i was at with this when asking question. Pretty much emulates your class above to some extent. Just feel there is a way given the binary nature of this that... one of those my god you make it look so simple answers pops up. – batFINGER Oct 07 '20 at 17:40
  • Sorry to disappoint you ;) I made another small edit, but I can not think of a more compact way to do this – scleronomic Oct 07 '20 at 18:16
  • No, no not disappointed. Simply feel there is a nice way to interface the indices to the 8 coordinates. but so far (due to my lack of ability) every turn has a catch. An abstract method that yields on leaf nodes was my latest failed attempt. Not that it's a big deal here re speed , (could be for 100, 000 bboxes) was aiming not to "swizzle" in the _`_init__` since the vert indices result is always same. – batFINGER Oct 07 '20 at 18:54
  • Ah I didn't think of speed till now, maybe I will find some time to have another look. – scleronomic Oct 07 '20 at 19:13
1

Here is another solution using an iterative approach to create a dictionary:

import numpy 
import itertools

directions = ['left', 'right', 'front', 'back', 'bottom', 'top']
dims = np.array([  0,       0,       1,      1,        2,     2])  # xyz

def get_vertices(bfl, tbr, x):
    g = np.array((bfl, tbr)).T
    return [v for v in itertools.product(*[g[ii][x[ii]] for ii in range(3)])]


bfl = [-1, -1, -1]
tbr = [1, 1, 1]

d = {}
for i in range(6):
    x = [[0, 1], [0, 1], [0, 1]]
    x[i//2] = [i % 2]  # x[dim[i] = min or max  
    d_i = dict(c=np.mean(get_vertices(bfl=bfl, tbr=tbr, x=x), axis=0))

    for j in np.nonzero(dims != dims[i])[0]:
        x[j//2] = [j % 2]
        d_ij = dict(c=np.mean(get_vertices(bfl=bfl, tbr=tbr, x=x), axis=0))

        for k in np.nonzero(np.logical_and(dims != dims[i], dims != dims[j]))[0]:
            x[k//2] = [k % 2]

            d_ij[directions[k]] = dict(c=np.mean(get_vertices(bfl=bfl, tbr=tbr, x=x), axis=0))
        d_i[directions[j]] = d_ij
    d[directions[i]] = d_i


d
# {'left': {'c': array([-1.,  0.,  0.]),
#    'front': {'c': array([-1., -1.,  0.]),
#      'bottom': {'c': array([-1., -1., -1.])},
#      'top': {'c': array([-1., -1.,  1.])}},
#    'back': {'c': array([-1.,  1.,  1.]),
#      'bottom': {'c': array([-1.,  1., -1.])},
#      'top': {'c': array([-1.,  1.,  1.])}}, 
#   ....

You can combine this with your linked question to access the keys of the dict via d.key1.key2.

scleronomic
  • 4,392
  • 1
  • 13
  • 43
1

Where I got to with this.

Have added this as an answer in some way to explain my question better

Looping over the 8 verts of the cube matches the 3 names to each valid corner.

The "swizzle" is a permutation of the three axis directions that make up the corners.

Feeding directly into a self nesting dictionary d[i][j][k] = value is a pain free way to create them. (pprint(d) below)

Happy to this point from there it turns ugly with some duck typing getting and getting the element indices from the simple 8 vert truth table.

For no particular reason made the method that returns the generated class a wrapper tho I'm not using it as such.

import numpy as np
import pprint
import operator
from itertools import product, permutations
from functools import reduce
from collections import defaultdict


class NestedDefaultDict(defaultdict):
    def __init__(self, *args, **kwargs):
        super(NestedDefaultDict, self).__init__(NestedDefaultDict, *args, **kwargs)

    def __repr__(self):
        return repr(dict(self))


def set_by_path(root, items, value):
    reduce(operator.getitem, items[:-1], root)[items[-1]] = value


def create_bbox_swizzle(cls, dirx=("left", "right"), diry=("front", "back"), dirz=("bottom", "top")):
    d = NestedDefaultDict()
    data = {}
    for i, cnr in enumerate(product(*(dirx, diry, dirz))):
        vert = {"index": i}
        data[frozenset(cnr)] = i
        for perm in permutations(cnr, 3):
            set_by_path(d, perm, vert)
    pprint.pprint(d)

    def wire_up(names, d):
        class Mbox:
            @property
            def co(self):
                return self.coords[self.vertices].mean(axis=0)
            def __init__(self, coords):
                self.coords = np.array(coords)
                self.vertices = [v for k, v in data.items() if k.issuperset(names)]
                pass

            def __repr__(self):
                if len(names) == 1:
                    return f"<BBFace {self.vertices}/>"
                elif len(names) == 2:
                    return f"<BBEdge {self.vertices}/>"
                elif len(names) == 3:
                    return f"<BBVert {self.vertices}/>"
                return "<BBox/>"
            pass

        def f(k, v):
            def g(self):
                return wire_up(names + [k], v)(self.coords)
            return property(g)

        for k, v in d.items():
            if isinstance(v, dict):
                setattr(Mbox, k, (f(k, v)))
            else:
                setattr(Mbox, k, v)
        return Mbox
    return wire_up([], d)


@create_bbox_swizzle
class BBox:
    def __init__(self, *coords, **kwargs):
        pass

Test drive:

>>> bbox = BBox(coords)  # used coords instead of corners
>>> bbox.co
array([ 5.96046448e-08, -1.19209290e-07,  0.00000000e+00])

>>> bbox.left.bottom
<BBEdge [0, 2]/>

>>> bbox.left.bottom.vertices
[0, 2]

>>> bbox.left.bottom.co
array([-1.00000036e+00, -1.19209290e-07,  0.00000000e+00])
batFINGER
  • 484
  • 2
  • 10