Inserting an object in any valid orientation

So I’m making a puzzle game. Sometimes, in order to power puzzle elements, the user needs to insert a battery into a slot.

The battery:

The slot:

When they are at the same position and rotation as one another, they sit perfectly flush:

I have a mechanic where if the battery gets close enough to the slot, and in the “close enough” orientation, the battery automatically slides into the slot. I accomplish this by having two empty gameobject “connection points” on either end of both the battery and the slot. As long as each connection point on the battery is within a predefined threshold away from a connection point on the slot, then I have the battery automatically slide into the slot. Here’s the code for moving into the slot. I’m lazy and I just parent the battery to the slot then lerp its position and rotation to zero:

IEnumerator InsertInsertable(InsertableBehaviour insertMe) {
    if (inserted) {
        Debug.LogError($"Tried to attach an insertable to {name} but there is already one attached: {inserted.name}", gameObject);
        yield break;
    }

    inserted = insertMe;
    var irb = insertMe.GetComponent<Rigidbody>();
    irb.isKinematic = true;
    insertMe.transform.parent = transform;
    var currentPosition = insertMe.transform.localPosition;
    var currentRotation = insertMe.transform.localRotation;
    var targetPosition = Vector3.zero;
    var targetRotation = Quaternion.identity;
    float time = 0;
    while (time < LerpDurationOnAttach) {        
        time += Time.deltaTime;
        var t = time / LerpDurationOnAttach;
        insertMe.transform.localPosition = Vector3.Lerp(currentPosition, targetPosition, t);
        insertMe.transform.localRotation = Quaternion.Lerp(currentRotation, targetRotation, t);

        yield return null;
    }

    OnSlotInserted?.Invoke(this, true, insertMe);
}

This all works great except for one issue… If the battery is coming in “upside down” or rotated along its long axis, the battery flips all the way around to its default orientation, which looks really unnatural:

I want the battery to be able to rest in the slot in any of 8 orientations: flipped either way, and with any of the 4 square sides sticking out of the slot. I want the battery to automatically choose the closest of these orientations to its current orientation (relative of course to the rotation of the slot) and go into the slot that way, so that it looks the most natural. Anyone have any ideas on how to best script that?

Ok so I thought of a solution. I’m not sure how optimal it is but it works!

First, I added a set of empty GameObjects as children of the battery to represent valid orientations for the battery to fit into the slot:

Each one simply has a local rotation equal to one of the 8 valid local rotations for my object. For example, here’s the one that’s flipped all the way around and rotated 90 degrees:

I added an array of Transforms to the component added all of the valid orientation objects to it:

[SerializeField]
Transform[] _validOrientations;

Then I made this method that simply compares each orientation to the current orientation of the battery, and figures out which one is the closest. I’m not sure if Quaternion.Angle is the most efficient way to do this, but it’s what I could think of:

public Quaternion ClosestValidOrientation() {
    Quaternion currentOrientation = transform.localRotation;

    if (_validOrientations == null || _validOrientations.Length < 1) {
        return Quaternion.identity;
    }

    Quaternion closest = Quaternion.identity;
    float closestAngle = float.PositiveInfinity;
    foreach (var candidate in _validOrientations) {
        float angle = Quaternion.Angle(candidate.localRotation, currentOrientation);
        if (angle < closestAngle) {
            closestAngle = angle;
            closest = candidate.localRotation;
        }
    }

    return closest;
}

Then back in my code to lerp the battery into position, I simply changed one line from var targetRotation = Quaternion.identity;tovar targetRotation = insertMe.ClosestValidOrientation();.

And now it’s working!