"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()
    
    


  • @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



  • @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! :)


Log in to reply