"Frame Selected Elements" command not working when running script through c4dpy?

Hi, I'm a Cinema 4D veteran but I'm very new to Python and coding in general.

I've made a little script that imports an .obj file into a specific scene, frames the geometry, and then renders it. When I run it in script manager it works great. However, when I run it via c4dpy/commandline, everything works fine except for the "Frame Selected Elements" command.

c4d.CallCommand(13038) # Frame Selected Elements

It just seems to ignore that command and then goes on to render the .obj at whatever size it was imported as.

As I said, I'm still new to programming/scripting, but I'm assuming that since there's no GUI/viewport when running a script through c4dpy, the Frame Selected Elements command simply isn't available. I could be completely wrong though.

Does anyone know if there's a workaround for this, or if I'm simply tackling the problem the wrong way?

Maybe this script helps you to frame your objects manually. The called helper methods should be self-explanatory.
If you have any questions, feel free to ask.

import c4d
import sys
import math
 
from .. import Helpers
 
 
class FramedCamera(object):
    """
    Modifies a given camera to frame a given object.
    The object to frame must be a clone since it is modified
    """
 
 
    def __init__(self, frame_obj, camera, azimuth=00, elevation=0):
        self.frame_object(frame_obj, camera, azimuth, elevation)
 
    # calculate the bounding sphere based on the camera FOV
 
    def calc_bsphere(self, op, camera):
        """Calculates a bounding sphere from a given object considering the FOV
 
        Args:
            op (c4d.BaseObject): The object to calculate the bounding sphere from
            camera (c4d.BaseObject): The camera to get the FOV from
 
        Returns:
            list: The bounding sphere center and radius
        """
 
        points = Helpers.GetAllPoints(op)
        if not points:
            raise ValueError("Object {} has no points".format(op.GetName()))
 
        p_min = c4d.Vector(sys.float_info.max)
        p_max = c4d.Vector(-sys.float_info.max)
 
        for p in points:
            p_min = c4d.Vector(min(p.x, p_min.x), min(p.y, p_min.y), min(p.z, p_min.z))
            p_max = c4d.Vector(max(p.x, p_max.x), max(p.y, p_max.y), max(p.z, p_max.z))
 
        center = (p_min + p_max) * 0.5
        radius = 0
        for p in points:
            radius = max(radius, (center - p).GetLength())
 
        fov_h = camera[c4d.CAMERAOBJECT_FOV]
        fov_v = camera[c4d.CAMERAOBJECT_FOV_VERTICAL]
        radius = (radius * 1.1) / math.sin(max(fov_h, fov_v) * 0.5)
        return center, radius
 
    # LookAt function
    def look_at(self, origin, target):
        """
        Creates an orientation matrix from two vectors
 
        Args:
            origin (c4d.Vector): The Vector to be used to orient
            target (c4d.Vector): The target vector
 
        Returns:
            c4d.Matrix: The orientation matrix
        """
        mat = c4d.Matrix()
        up_temp = c4d.Vector(0, 1.0, 0)
 
        v_fwd = (target - origin).GetNormalized()
        v_right = up_temp.Cross(v_fwd).GetNormalized()
        v_up = v_fwd.Cross(v_right).GetNormalized()
 
        mat.off = origin
        mat.v1 = v_right
        mat.v2 = v_up
        mat.v3 = v_fwd
 
        return mat
 
    # frame objects
    def frame_object(self, op, camera, elevation, azimuth):
        """
        Places and orients a camera to fit a given object An optional angle (H,P) can be given.
 
        Args:
            op (c4d.BaseObject): The object to frame
            camera (c4d.BaseObject): The camera that is used
            elevation (float): Camera Heading, in radians
            azimuth (float): Camera Pitch, in radians
 
        Returns:
            bool: True for success, False otherwise.
        """
 
        Helpers.convertInstances(op)
 
        # Get bounding sphere depending on camera FOV
        b_sphere = self.calc_bsphere(op, camera)
        if not b_sphere:
            raise ValueError("Bounding sphere cannot be calculated")
 
        # Set camera position and direction
        center, radius = b_sphere
        camera.SetMg(c4d.Matrix())
        cam_pos = c4d.Vector(center)
        cam_pos.x = cam_pos.x + math.cos(azimuth) * math.sin(elevation) * radius
        cam_pos.y = cam_pos.y + math.sin(azimuth) * radius
        cam_pos.z = cam_pos.z + math.cos(azimuth) * math.cos(elevation) * -radius
 
        camera.SetMg(self.look_at(cam_pos, center))

Side note. This code comes from a prototype, therefore it is not optimized/comes with some syntactical flaws.

Hi @OblivionDawn,

thank you for reaching out to us and thank you @mp5gosu for providing a workaround. Principally speaking, cdpy comes with BaseDraw instances, i.e., BaseView instances, i.e., view ports and a camera attached to them, which is required to carry out the framing of an object. But since c4dpy is GUI-less, they are being initialized with a "null"-frame so to speak, i.e., a frame with zero dimensions. The script attached to the end of the post will spit out something like this:

Active BaseDraw safe-frame: {'cl': 0, 'ct': 0, 'cr': 0, 'cb': 0}

Although unlikely, this could be a reason why 'Frame Selected Elements' is failing for you. It will probably take me some time to investigate this and all possibilities of failure. I will report back once I have come to a conclusion.

Cheers,
Ferdinand

The little c4dpy script:

import c4d
import os


def main():
    """
    """
    path = os.path.split(__file__)[0]
    file = os.path.join(path, "cube.c4d")
    doc = c4d.documents.LoadDocument(file, c4d.SCENEFILTER_OBJECTS, None)
    if doc is None:
        raise RuntimeError("Could not load document.")
    bd = doc.GetActiveBaseDraw()
    if bd is not None:
        print ("Active BaseDraw safe-frame:", bd.GetSafeFrame())
    c4d.documents.InsertBaseDocument(doc)
    c4d.documents.SetActiveDocument(doc)
    c4d.CallCommand(13038)
    doc = c4d.documents.GetActiveDocument()
    print ("Active document state:", doc)
    file = os.path.join(path, "cube_mod.c4d")
    res = c4d.documents.SaveDocument(doc, file, c4d.SAVEDOCUMENTFLAGS_NONE,
                                     c4d.FORMAT_C4DEXPORT)
    print (res)


if __name__ == '__main__':
    main()

MAXON SDK Specialist
developers.maxon.net

@mp5gosu, thanks for that workaround, I'll give it a shot and let you know if I have any questions.

@ferdinand, interesting, thanks for looking into this issue and I look forward to hearing what you come up with.

Hi @OblivionDawn,

so I had a little debug session with c4dpy and the outcome is that the command is not bugged, but does not meet the requirements to be run under c4dpy.

While framing an object technically does not require a viewport, but only a camera with its parameters like projection and field of view, the "Frame Selected Elements" command factually does require a viewport. The reason is that the command is grouped together with a whole architecture of viewport commands which first check if they can get hold of the active viewport. If not, they will bail their execution. This will fail in c4dpy due to the fact that c4dpy is being run GUI-less.

Due to this being bound into the mentioned architecture of viewport commands we consider this to be an acceptable limitation of c4dpy and not a bug and therefore will not modify this behaviour. We have to ask you to resort to a custom object framing solution. @mp5gosu has already given you a nice example for such a solution (thanks again 😄).

edit: Regarding the dead documents (I modified my previous posts to prevent confusing future readers), I was just a bit stupid and forgot to call c4d.documents.InsertDocument before calling c4d.documents.SetActiveDocument which causes all sorts of hiccups in Cinema, not only c4dpy. We also found out that the Python docs do not mention this requirement and will update them accordingly.

Thank you for your understanding,
Ferdinand

MAXON SDK Specialist
developers.maxon.net

@ferdinand said in "Frame Selected Elements" command not working when running script through c4dpy?:

forgot to call c4d.documents.InsertDocument before calling c4d.documents.SetActiveDocument which causes all sorts of hiccups in Cinema, not only c4dpy

This is a real classic! I stumbled over this several times! 🙂