Gumball's centre (or math behind it) for multiple objects in rhino3dm

Hi.

I’m trying to rotate objects through rhino3dm.

In Rhino, I would select these objects and rotate around Gumball’s centre, and this centre is right where I expect it to be.

I’m trying to recreate the possible logic behind Gumball’s centre through:

1. bounding_box_tight = culled_object.Geometry.GetTightBoundingBox() #tight boudning box isn't present in the API. Taken from the forums., through an average of their centres, which results in a point at (46.948,0.399,97.035)

2. bounding_box_loose = culled_object.Geometry.GetBoundingBox(), averaged centres, the result is a point at (47.013,0.410,97.030)

3. Through their BoundingBox.Cetres, that return the following points for Tight and Loose boxes respectively: (48.171,0.334,97.511) and (48.201,0.334,97.506)

However, neither of the points is close enough, let alone precisely at the centre of the Gumball.

The points and Gumball’s centre position can be seen in the image below:

The question is, if anyone could elaborate, how does Rhino calculate the position for Gumball? Can this behaviour be recreated via rhino3dm?

The bounding box is calculated in the Construction Plane and not the world frame. Rhino uses the center of the tight bounding boxes for all the objects. Meaning each objects tight bounding box is unioned with the others. The center of the that unioned bounding box is the gumballs origin. Here’s an example in the script editor

// #! csharp
using System;

var doc = Rhino.RhinoDoc.ActiveDoc;

var objs = doc.Objects.GetSelectedObjects(false, false);
var plane = doc.Views.ActiveView.ActiveViewport.ConstructionPlane();

var bbox = Rhino.Geometry.BoundingBox.Unset;
foreach(var obj in objs)
{
  var objBox = obj.Geometry.GetBoundingBox(plane);
  bbox.Union(objBox);
}

var center = bbox.Center;
doc.Objects.AddPoint(plane.PointAt(center.X, center.Y, center.Z));

Thank you very much, it makes sense. I was testing it in Grasshopper and was just writing my report as you have finished yours..!

I was too quick to declare the problem solved.

The result is infinitely better, but still not the same:

Perhaps due to me being unable to initiate an empty box in Python and having to resort to bounding_box_tight_united = rhino3dm.BoundingBox(0,0,0,0,0,0)

and

for entry_value in names_with_ids.values():
    print(entry_value)
    culled_object = models.Objects.FindId(entry_value)
    bounding_box_tight = culled_object.Geometry.GetTightBoundingBox()
    bounding_box_tight_united = rhino3dm.BoundingBox.Union(bounding_box_tight_united, bounding_box_tight)

Obtained result: 42.4574,-1.79918,108.113

Expected result: 42.439929,-1.786969,108.118359

───────────────────────────────────────────────────────────

Hmm, no…

bounding_box_tight_united = None


for entry_value in names_with_ids.values():
    print(entry_value)
    culled_object = models.Objects.FindId(entry_value)
    bounding_box_tight = culled_object.Geometry.GetTightBoundingBox()

    if bounding_box_tight_united == None:
        bounding_box_tight_united = bounding_box_tight
    else:
        bounding_box_tight_united = rhino3dm.BoundingBox.Union(bounding_box_tight_united, bounding_box_tight)

Still results in centre at: 42.4574,-1.79918,108.113

from what I can see, you’re not using the construction plane when asking for the bounding box.

Sorry, I missed that detail…

Regardless, now I tried passing both a random plane and a set CPlane to the GetTightBoundingBox:

CPlane = rhino3dm.Plane.WorldXY()

CPlane_real = rhino3dm.ConstructionPlane()

CPlane_real.Plane = CPlane

bounding_box_tight = culled_object.Geometry.GetTightBoundingBox(CPlane_real)

And it throws the following error:
TypeError: GetTightBoundingBox(): incompatible function arguments. The following argument types are supported:
1. (self: rhino3dm._rhino3dm.GeometryBase) → rhino3dm._rhino3dm.BoundingBox

Invoked with: <rhino3dm._rhino3dm.Mesh object at 0x000002B111EE08F0>, <rhino3dm._rhino3dm.ConstructionPlane object at 0x000002B111D1F970>

hmm, looking at the API docs, there seems to be no overload that takes a plane in rhino3dm (yet)

Oh no… Thank you for looking into it. I hope this overload can soon be introduced.

you might be able to work around it. (disclaimer: Claude generated sample, I haven’t tested this but looking at it, this should work)

def combined_bbox_center(geometries, plane):
    to_plane = rhino3dm.Transform.ChangeBasis(rhino3dm.Plane.WorldXY(), plane)
    to_world = rhino3dm.Transform.ChangeBasis(plane, rhino3dm.Plane.WorldXY())

    combined = None
    for geo in geometries:
        copy = geo.Duplicate()
        copy.Transform(to_plane)
        bbox = copy.GetTightBoundingBox()
        combined = bbox if combined is None else rhino3dm.BoundingBox.Union(combined, bbox)

    if combined is None or not combined.IsValid:
        return None

    center_pt = rhino3dm.Point(combined.Center)
    center_pt.Transform(to_world)
    return center_pt.Location

BoundingBox doesn’t pass the plane used for its construction, so I wasn’t able to pass a proper ‘plane’ into the function. I constructed one through projections of BoundingBox’s .Min and Max coordinates.

ChaneBasis didn’t work out, because it appears it doesn’t exist in rhino3dm.

I tried to change it to PlaneToPlane reorientation instead:

def combined_bbox_center(geometries, plane):

    to_plane = rhino3dm.Transform.PlaneToPlane(rhino3dm.Plane.WorldXY(), plane)

    to_world = rhino3dm.Transform.PlaneToPlane(plane, rhino3dm.Plane.WorldXY())


    combined = None

    for geo in geometries:

        copy = geo.Duplicate()

        copy.Transform(to_plane)

        bbox = copy.GetTightBoundingBox()

        combined = bbox if combined is None else rhino3dm.BoundingBox.Union(combined, bbox)


    if combined is None or not combined.IsValid:

        return None


    center_pt = rhino3dm.Point(combined.Center)

    center_pt.Transform(to_world)

    return center_pt.Location

Plane is obtained through:

bb_united_min = bounding_box_tight_united.Min

bb_united_max = bounding_box_tight_united.Max

bb_united_centre = bounding_box_tight_united.Center


bb_united_max.Z = bb_united_min.Z

print("Min:", bb_united_min)

print("Max projected:", bb_united_max)

bb_united_centre.Z = bb_united_min.Z

print("Centre:", bb_united_centre)

bb_united_plane = rhino3dm.Plane(bb_united_centre,bb_united_max,bb_united_min)

First of all, plane resulted in Box’s plane: | 42.4574,-1.79918,-0.693389 (origin) | 0.699048,0.715074,0 (x axis) | 0,0,0 (y axis) | 0,0,0 (z axis)

While the coordinates provided were as follows:
Centre: 42.4574,-1.79918,-0.693389 (origin)
Min: -110.943,-158.716,-0.693389 (y)
Max projected: 195.858,155.118,-0.693389 (x)

And the expected plane is: 42.4574,-1.79918,-0.693389 (origin) | -0.699049182945316,-0.715073590495052,0 (x axis) | -0.715073590495052,0.699049182945316,0 (y axis) | 0,0,-1 (z axis)

as shown in Grasshopper example below:

I thought maybe Rhino3dm confused get and set while obtaining coordinates:

bb_united_centre_Z = bb_united_centre.Z = bb_united_min.Z

bb_united_max_Z = bb_united_max.Z = bb_united_min.Z

print("BB test: ", bb_united_centre_Z, bb_united_max_Z, sep=" | ")

And it does: BB test: | 42.4574,-1.79918,-0.693389 | -0.693388621860773,
and you can’t set their classes like this, because Point3d() doesn’t accept Point3d().

bb_united_centre = rhino3dm.Point3d()

bb_united_max_Z = rhino3dm.Point3d()

Regardgless, print("BB test: ", bb_united_centre, bb_united_max, sep=" | ")
results in BB test: | 42.4574,-1.79918,-0.693389 | 195.858,155.118,-0.693389, so coordinates are fine.

The coordinates immediately get passed to the Plane() constructor,
bb_united_plane = rhino3dm.Plane(bb_united_centre,bb_united_max,bb_united_min)

Swapping coordinates results in:
Box’s plane: | 42.4574,-1.79918,-0.693389 | -0.699048,-0.715074,0 | 0,0,0 | 0,0,0

and doesn’t affect the transformation.

It would seem the issue appears during Plane()'s calculations.