Navigation

    • Register
    • Login
    • Search
    1. Home
    2. dskeithbuck
    dskeithbuck

    dskeithbuck

    @dskeithbuck

    Creative Technologist at Buck Design. This is my work account.

    17
    Reputation
    37
    Posts
    355
    Profile views
    0
    Followers
    0
    Following
    Joined Last Online
    Website buck.tv/ Location Los Angeles, CA

    • Profile
    • More
      • Following
      • Followers
      • Topics
      • Posts
      • Best
      • Groups
    dskeithbuck Follow

    Best posts made by dskeithbuck

    Compiling the Cinema 4D R20 C++ SDK Examples

    Compiling the Cinema 4D R20 C++ SDK Examples

    Background

    I've been developing Cinema 4D plugins using the Python API for a long time, and have attempted (but often struggled) to do development with Cinema 4D's C++ API. One of my biggest stumbling blocks was just getting the example plugins to compile. So, in this post, I'm going to try and catalog the steps I've taken to get this working.

    Reference

    I'm making extensive use of the Cinema 4D C++ SDK Documentation, and it should be seen as the definitive source of the information I'm providing in a simplified format here. Specifically, I found the following pages/articles quite helpful:

    • Getting Started: Introduction : Cinema 4D C++ SDK
    • Project Tool : Cinema 4D C++ SDK

    Disclaimer

    I am by no means an expert, and I will likely not be approaching things in the best/most correct way. If you know of a better way to do something, or if I'm sharing incorrect info, please correct me!

    Pre-Requisites

    Modern programming relies on a complex of interconnected tools to do anything mildly useful. Some things that work great with one version of a tool, just won't work in the next version. So, I can only promise that this works with the exact setup I'm running, and if you find a workaround due to a difference in your setup, please post it.

    • Cinema 4D R20 SP1
    • Microsoft Windows 10 (I'm sure this can be done on a Mac, I just don't know how yet).
    • Microsoft Visual Studio Community 2017
      • Get a HelloWorld C++ Console Application working to verify Visual Studio is working period.
      • Note: It seems like VS 2015 is what's officially supported/required by the Maxon C++ SDK, so that might be the better choice but will differ from my instructions below.

    Unzip and Point Cinema 4D to the SDK

    1. Locate sdk.zip in your Cinema 4D installation directory.
      sdk.zip
      • Note: This is different from the SDK Examples GitHub Repo as it includes a critical frameworks directory.
      • If you don't have sdk.zip I think the only way to get it is to reinstall Cinema 4D.
      • I recommend keeping a backup of this .zip file somewhere.
    2. Duplicate sdk.zip into a path that is easy to access via terminal. e.g. C:\sdk.zip
      • I'm duplicating the .zip to avoid accidentally deleting it as it's hard to recover a clean version without a reinstall of C4D.
      • You may need admin rights on your machine to do this.
    3. Unzip the SDK to C:\sdk.
      0_1536173987690_495ba90d-c201-4579-bc17-c9f49b08df36-image.png
    4. Open Cinema 4D
    5. Edit > Preferences > Plugins
    6. Go to the Plugins tab and add the C:\sdk\plugins folder.
      Plugin Paths
      • Unlike previous version of Cinema 4D, R20 lets you point to multiple plugins directory, making development much simpler.

    If you restart Cinema 4D at this point, you may be disappointed to discover that no new plugins have shown up. That's because we need to...

    Generate a Visual Studio Project

    In previous versions of C4D you would have to have a keen understanding of Visual Studio to setup a project from scratch, or the patience to duplicate an existing working project and then rip out everything that doesn't do what you need. Maxon has provided a really great new tool that allows you to build a working Visual Studio project file from a single source directory so long as it includes a specifically named and formatted .txt file.

    1. Download the latest version of the C++ Project Tool from developers.maxon.net.
    2. Unzip the project tool to C:\projecttool, ensure that this directory contains the actual project tool and libs, not just another nested folder.
      Project Tool Directory
    3. Open the command prompt (Windows Key, then type "command", and hit enter).
    4. In the command prompt, type
      1. cd c:\projecttool
      2. kernel_app_64bit.exe g_updateproject=C:\sdk
        Project Tool Commands
    5. This should launch the project tool and build a project file for the contents of C:\sdk
      0_1536175506109_288bb334-aa29-4f6e-9457-588fc2794bcd-image.png
    6. Press return.
    7. In your file browser, navigate to and open C:\sdk\plugins\project\. If everything worked as expected, you should now see a file called plugins.sln

    You've now created a Visual Studio project file that could be used to compile your project. But there's still more we have to do.

    Add v140 Support to VC++

    1. In your file browser, navigate to and open C:\sdk\plugins\project\plugins.sln
      0_1536175570674_c5425a94-6593-4cc7-8b36-40c5db6d1b1a-image.png
    2. When you open it, you will likely discover a message that looks like this:
      0_1536182259322_bb81ca2b-29eb-4bac-80ae-74d92cb1b295-image.png
      This indicates that the project file doesn't match the latest and greatest from Microsoft. That's because the project was built for Visual Studio 2015, and we're using Visual Studio 2017. We'll need to modify our Visual Studio installation.
    3. Hit Cancel and Quit Visual Studio.
    4. Press the Windows key and search for "Visual Studio Installer".
      0_1536182505366_448b3a1d-97e7-443c-a4cc-414397cc0099-image.png
    5. Run the Installer.
    6. Press Yes if prompted to allow changes to your computer.
    7. Click on the Modify button.
      0_1536182561998_05005f68-6da5-4cec-83d0-c058e7e8d3ea-image.png
    8. Click on the Individual Components tab.
    9. Scroll down and check the VC++ 2015.3 v14.00 (v140) toolset for desktop option under the Compilers, Build Tools, and Runtimes header.
      0_1536182632860_0946a6e3-2432-48f0-a431-9a29f6e89ad6-image.png
    10. Click on the Modify button in the lower right.
    11. Allows the installation to complete - it could take a while, this is roughly 5gb of additional downloads.

    Build the Project

    1. In your file browser, navigate to and open C:\sdk\plugins\project\plugins.sln
      0_1536175570674_c5425a94-6593-4cc7-8b36-40c5db6d1b1a-image.png
    2. Visual Studio should re-open, only this time you won't see the window asking if you want to upgrade the project (if you do, select the options for both dropdowns that say something like Leave as Is).
    3. Go to Build > Build Solution or press F7.
      0_1536182963902_112e69ec-cf7d-4cf5-bbe9-6facf3772d8c-image.png
      • A bunch of text should appear in the Console and update.
      • The build process will likely take a few minutes, as VC++ is compiling the whole C4D SDK, future builds will likely be much quicker as only modified files will need to be completely rebuilt.
    4. Assuming everything went correctly, you should see:
      0_1536183170361_a3cddf0f-f6ad-4641-8f80-33b9098c5213-image.png
      • If you encounter errors, read them carefully and Google liberally. That's how I was eventually able to get it all working on my end (adding v140 support was the issue with my setup).

    Verify the Plugins Were Compiled

    1. Open Cinema 4D R20
    2. Plugins > Cinema 4D SDK
      0_1536183347392_0ba858e6-4588-46e4-99c3-c39eb0b42974-image.png

    Congratulations! You've successfully compiled the Cinema 4D SDK Example Plugins!

    posted in Cinema 4D SDK •
    How to Get the Selected Keyframes in Active Timeline/FCurve Manager

    How would you find these selected keyframes?
    0_1536620777786_60e72919-26ca-492c-a3a9-008da5740d27-image.png

    I didn't see a super clean forum post on the subject, so I figured I would write up my findings as I worked this out. Turns out what I thought was going to be a tutorial, is actually a question for the SDK Team.

    Open Questions

    1. How do you determine which Timeline is currently active?
    2. How can you detect selected keys in the F-Curve manager?
      • track.GetNBit(c4d.NBIT_TL1_FCSELECT) doesn't seem to be working.
    3. How do you determine if an object/track/keyframe is visible in the Active timeline? I don't want to act on hidden objects as the user wouldn't expect that to happen.

    Getting Selected Keyframes

    Well, unlike Objects and Materials, we don't have any helper functions like BaseDocument.GetActiveObjects() and BaseDocument.GetActiveMaterials(). So, we have to:

    1. Iterate through every object in the scene.
    def GetNextObject(op):
        """Gets the next object in the scene graph after `op`. Does not stop until all objects are returned.
        From: https://c4dprogramming.wordpress.com/2012/11/26/non-recursive-hierarchy-iteration/
        """
    
        if op==None:
            return None
    
        if op.GetDown():
            return op.GetDown()
    
        while not op.GetNext() and op.GetUp():
            op = op.GetUp()
    
        return op.GetNext()
    
    def IterAllObjects(doc):
        """Iterates through all objects in `doc`.
        Call like:
        for obj in IterAllObjects(doc):
            print obj.GetName()
        """
    
        if doc is None:
            return
    
        op = doc.GetFirstObject()
        while op is not None:
            yield op
            op = GetNextObject(op)
    
    1. Iterate through each object's tracks.
    def IterAllTracks(doc):
        """Iterates through all animation tracks in `doc`.
        Call like:
        for track in IterAllTracks(doc):
            print track.GetName()
        """
        
        if doc is None:
            return
    
        for obj in IterAllObjects(doc):
            for track in obj.GetCTracks():
                yield track
    
    1. Get the track's Curve.
    curve = track.GetCurve()
    if curve is None:
        continue
    
    1. Iterate through each Curve's keys.
    for key_id in xrange(curve.GetKeyCount()):
        key = curve.GetKey(key_id)
    
    1. Test each key to see if it is selected.
        is_selected = key.GetNBit(timeline_bit)  # Typically: c4d.NBIT_TL1_SELECT
    
    1. Push selected keys into a list.
    2. Return the list.

    Current Script

    """Name-en-US: Print Selected Keys
    Description-en-US: Prints the frame and value of the selected keys to the console."""
    
    import c4d
    from c4d import gui
    
    def GetNextObject(op):
        """Gets the next object in the scene graph after `op`. Does not stop until all objects are returned.
        From: https://c4dprogramming.wordpress.com/2012/11/26/non-recursive-hierarchy-iteration/
        """
    
        if op==None:
            return None
    
        if op.GetDown():
            return op.GetDown()
    
        while not op.GetNext() and op.GetUp():
            op = op.GetUp()
    
        return op.GetNext()
    
    def IterAllObjects(doc):
        """Iterates through all objects in `doc`.
        Call like:
        for obj in IterAllObjects(doc):
            print obj.GetName()
        """
    
        if doc is None:
            return
    
        op = doc.GetFirstObject()
        while op is not None:
            yield op
            op = GetNextObject(op)
    
    def IterAllTracks(doc):
        """Iterates through all animation tracks in `doc`.
        Call like:
        for track in IterAllTracks(doc):
            print track.GetName()
        """
        
        if doc is None:
            return
    
        for obj in IterAllObjects(doc):
            for track in obj.GetCTracks():
                yield track
    
    def GetActiveTracks(doc, timeline_bit=c4d.NBIT_TL1_SELECT):
        """Returns the active tracks based on `selection_bit`.
        
        NBIT Reference:
        https://developers.maxon.net/docs/Cinema4DPythonSDK/html/modules/c4d/C4DAtom/GeListNode/index.html?highlight=getnbit#GeListNode.GetNBit
        
        Timeline 1: c4d.NBIT_TL1_SELECT,
        Timeline 2: c4d.NBIT_TL2_SELECT,
        Timeline 3: c4d.NBIT_TL3_SELECT,
        Timeline 4: c4d.NBIT_TL4_SELECT,
        
        F-Curve 1: c4d.NBIT_TL1_FCSELECT,
        F-Curve 2: c4d.NBIT_TL2_FCSELECT,
        F-Curve 3: c4d.NBIT_TL3_FCSELECT,
        F-Curve 4: c4d.NBIT_TL4_FCSELECT
        
        Issues:
        * No way (that I'm aware of) to detect which timeline is active.
        * No way to determine whether in Timeline or FCurve mode.
        * NBIT_TL1_FCSELECT seems to return incorrect values.
        * Not aware of whether track is visible/hidden in Timeline or not.
        """
    
        if doc is None:
            return
        
        active_tracks = []
    
        for track in IterAllTracks(doc):
            if track.GetNBit(timeline_bit):
                active_tracks.append(track)
                
        return active_tracks
    
    def GetActiveKeys(doc, timeline_bit=c4d.NBIT_TL1_SELECT):
        """Returns all active keys.
        
        NBIT Reference:
        https://developers.maxon.net/docs/Cinema4DPythonSDK/html/modules/c4d/C4DAtom/GeListNode/index.html?highlight=getnbit#GeListNode.GetNBit
        
        Timeline 1: c4d.NBIT_TL1_SELECT,
        Timeline 2: c4d.NBIT_TL2_SELECT,
        Timeline 3: c4d.NBIT_TL3_SELECT,
        Timeline 4: c4d.NBIT_TL4_SELECT,
        
        F-Curve 1: c4d.NBIT_TL1_FCSELECT,
        F-Curve 2: c4d.NBIT_TL2_FCSELECT,
        F-Curve 3: c4d.NBIT_TL3_FCSELECT,
        F-Curve 4: c4d.NBIT_TL4_FCSELECT
        """
        
        active_keys = []
    
        for track in IterAllTracks(doc):
            curve = track.GetCurve()
            if curve is None:
                continue
    
            for key_id in xrange(curve.GetKeyCount()):
                key = curve.GetKey(key_id)
                is_selected = key.GetNBit(timeline_bit)
                if (key is not None) and is_selected:
                    active_keys.append(key)
    
        return active_keys
    
    def main():
        """Print the frame and value of the currently selected keys to the Python Console.
        """
        
        print " "
        print " "
        print "Active Keys"
        print "==========="
    
        active_keys = GetActiveKeys(doc)
        if active_keys:
            for key in GetActiveKeys(doc, c4d.NBIT_TL1_SELECT):
                print key.GetTime().GetFrame(doc.GetFps()), ": ", key.GetValue()
        else:
            print "No active keys."
    
    if __name__=='__main__':
        main()
    

    Thanks,

    Donovan

    --- Edits ---

    • Fixed issue where I was only iterating through some of the tracks.
    posted in Cinema 4D SDK •
    RE: How to use c4dpy for Cinema4D R20 SP1

    Okay, I can't promise this is the correct way to do this, but it seems to have worked for me:

    Install C4D Py

    1. Ensure that you have Cinema 4D R20+ installed, I don't think this will work without it.
    2. In your C4D install directory, unzip the sdk.zip file. (Note: this step might not be strictly necessary, not sure.)
    3. Download the latest c4dpy, ensure that it corresponds with your version of C4D.
      • You may need to download the latest C4D Service Pack via the Online Updater (C4D > Help > Check for Updates...)
    4. Unzip the c4dpy .zip file and copy the contents into your C4D install directory. C4dpy seems to rely on your C4D install's library files and will spit out errors if you don't do this.
      0_1536168428071_2de2a374-802d-4356-900b-bf6376bd4300-image.png

    Verify the Installation

    Mac

    Note: I haven't tested on mac, so I'm not sure this works.

    1. Cmd + Space then Terminal
    2. Drag the c4dpy.app file into the terminal - you should get the path to the file.

    PC

    1. Open terminal on Mac or Command Prompt on PC
    2. Drag the c4dpy.exe file into the Command Prompt

    Both

    1. Press enter/return
    2. You should see something like:
    C:\Users\donovan>"C:\Program Files\MAXON\Cinema 4D R20\c4dpy.exe"
    Welcome to the world of C4D and Python 2.7.14 (default, May  3 2018, 18:05:57) [MSC v.1500 64 bit (AMD64)] (c) 2018
    

    Setup your IDE

    Instructions will vary a bit on a per IDE basis.

    Create a test script (All IDEs)

    1. Open C4D
    2. Script > Script Manager
    3. Script Manager > File > New.... You should see something like:
    import c4d
    from c4d import gui
    # Welcome to the world of Python
    
    
    # Script state in the menu or the command palette
    # Return True or c4d.CMD_ENABLED to enable, False or 0 to disable
    # Alternatively return c4d.CMD_ENABLED|c4d.CMD_VALUE to enable and check/mark
    #def state():
    #    return True
    
    # Main function
    def main():
        gui.MessageDialog('Hello World!')
    
    # Execute main()
    if __name__=='__main__':
        main()
    
    1. Script Manager > File > Save...
    2. Save the script as helloworld.py.
    3. Open your scripts folder: C4D > Script > User Scripts > Script Folder...
    4. Open the helloworld.py in your IDE.

    PyCharm

    0_1536094384719_8cdcbe43-0e13-4d8c-9de7-98521145bbc0-image.png

    Configure your Intepreter

    Additional C4D Py Prep

    1. PyCharm requires that Python Interpreters be named python.exe so rename the c4dpy.exe file in your C4D Directory to python.exe. (Note: I'm not sure if this breaks any of C4D Python's functionality).

    In PyCharm

    1. File > Settings > Project > Interpreter
      0_1536094776230_769af828-0d7a-4753-beaa-94301fe0e2eb-image.png
    2. Click on the Project Interpreter dropdown (probably says Python 2.7) and select "Show All".
      0_1536094837673_40027c3a-638a-4a41-bc6c-132bcecd46bc-image.png
    3. Click on the + to add a new interpreter.
    4. Click on System Interpreter
    5. Browse to the location of the renamed c4dpy.exe (now called python.exe).
    6. Choose OK. It will probably take a minute for the Interpreters list to update to include the c4dpy interpreter.
    7. Choose OK to select the Python 2.7 (1) interpreter (Or however your renamed c4dpy.exe interpreter is listed).
    8. In the text editor type from c4d import then hit ctrl + space trigger auto-complete.
      • You may see a status update about "Updating skeletons" this can take a few minutes the first time as PyCharm has to build up a map of the C4D sdk (<- I think that's what it's doing, not certain).
    9. Eventually, you should see something like:
      0_1536095233162_0aa181f4-664c-4abd-9aab-951251f4e47b-image.png

    Congratulations, you've now got AutoComplete working in PyCharm!

    Microsoft VS Code

    0_1536099876801_ed15bae1-6a61-4343-acf7-9074ec817b47-image.png

    Prerequisites

    If you can't get IntelliSense (autocomplete) working with a default Python install, it's probably not going to work with a C4D Py. If you can do the following, you're in good shape to work with C4D Py in VS Code.

    1. Install the Python VS Code Extension
    2. Follow the VS Code Python Tutorial.

    Configure VS Code

    1. Open VS Code
    2. File > Open Folder > ... Navigate to your C4D Scripts folder (You can use C4D > Scripts > User Scripts > Scripts Folder... to easily find it).
    3. Ctrl + Shift + P to search for : "workspace settings" (Note: this setting is only being adjusted for this folder).
      0_1536100794561_44027c89-7521-4f55-83f0-49acf9be3b4b-image.png
    4. In the worspace settings panel...
      0_1536100773150_8cedc76b-9a0f-4a94-a1da-b0a8ff83d601-image.png
    5. Start typing python.pythonPath then hit enter
      0_1536100876197_6817d75d-0406-4b13-867a-d2c559626c13-image.png
    {
        "python.pythonPath": "python"
    }
    
    1. Set the python path to the location of your c4dpy.exe file.
    {
        "python.pythonPath": "C:\Program Files\MAXON\Cinema 4D R20\c4dpy.exe"
    }
    

    Windows paths use a \ which needs to be escaped \\ or reversed / for the path to properly register in the VSCode Settings.

    {
        "python.pythonPath": "C:/Program Files/MAXON/Cinema 4D R20/c4dpy.exe"
    }
    
    1. File > Save Workspace (This will ensure the settings get remembered.
    2. Restart VS Code
    3. In helloworld.py in main(): type gui. and then press ctrl + space. You'll see:
      0_1536101234399_ab8f03a9-d9d7-4021-bb78-5be47a1596ac-image.png
    4. Wait a minute as VS code builds up its understanding of C4D's API, eventually, you should see:
      0_1536101334378_8ff2854d-979c-4f96-9ea3-395bd00d69aa-image.png

    Congratulations, you've now got AutoComplete working in VS Code!

    To the C4D Devs: If these instructions are accurate, and you haven't alread written something similar, feel free to include it in the SDK / C4D Py Docs.

    -- Edits --

    2018/09/05 10:27 - Added screenshot to clarify that you should copy the contents of the Unzipped file to your C4D directory.
    2018/09/04 15:50 - Added instructions for VS Code.

    posted in Cinema 4D SDK •
    Getting the State of Modifier Keys (ctrl/cmd, shift, alt)

    This is a snippet I like to use for easily accessing the state of modifier keys when a user runs a script. I figured I would post it here as I couldn't easily track it down when I needed it today and thought it might be useful to other folks as well.

    def get_modifiers():
        """Returns state of modifier keys: ctrl, shift, alt
        
        Example Usage:
        ctrl, shift, alt = get_modifiers()
        """
    
        bc = c4d.BaseContainer()
        if c4d.gui.GetInputState(c4d.BFM_INPUT_KEYBOARD,c4d.BFM_INPUT_CHANNEL,bc):
            ctrl = bc[c4d.BFM_INPUT_QUALIFIER] & c4d.QCTRL
            shift = bc[c4d.BFM_INPUT_QUALIFIER] & c4d.QSHIFT
            alt = bc[c4d.BFM_INPUT_QUALIFIER] & c4d.QALT
            
        return ctrl, shift, alt
    
    posted in General Talk •
    RE: How to Get the Selected Keyframes in Active Timeline/FCurve Manager
    1. TL1_HIDE (and TL2_ to TL4_) provide the hidden state of tracks in both dope sheet and F-curve mode. But as said before, you can just get this information for each of the up to four timeline managers, but not for a specific active one.

    Okay, so I can use this to filter my tracks list.

    Yeah, so this can tell me if a track has been manually hidden by the user using Timeline > View > Hide > Hide Selected Elements, but it does not show whether a given object is actually visible to the user in the timeline (unless it's been explicitly hidden). The timeline doesn't necessarily show all animated objects all the time, sometimes a user can setup their timeline so only 2-3 objects are visible, or 2-3 tracks out of dozens are visible - with none of them being explicitly "hidden".

    I assume the answer is no, but is there a way to get a filtered list of objects/tracks/keys that are visible to the end-user for a given timeline?

    Updated script including the ability to filter explicitly hidden objects, along with a method for detecting the active timeline.

    """Name-en-US: Print Selected Keys
    Description-en-US: Prints the frame and value of the selected keys to the console."""
    
    import c4d
    from c4d import gui
    
    def GetNextObject(op):
        """Gets the next object in the scene graph after `op`. Does not stop until all objects are returned.
        From: https://c4dprogramming.wordpress.com/2012/11/26/non-recursive-hierarchy-iteration/
        """
    
        if op==None:
            return None
    
        if op.GetDown():
            return op.GetDown()
    
        while not op.GetNext() and op.GetUp():
            op = op.GetUp()
    
        return op.GetNext()
    
    def IterAllObjects(doc):
        """Iterates through all objects in `doc`.
        Call like:
        for obj in IterAllObjects(doc):
            print obj.GetName()
        """
    
        if doc is None:
            return
    
        op = doc.GetFirstObject()
        while op is not None:
            yield op
            op = GetNextObject(op)
    
    def IterAllTracks(doc):
        """Iterates through all animation tracks in `doc`.
        Call like:
        for track in IterAllTracks(doc):
            print track.GetName()
        """
    
        if doc is None:
            return
    
        for obj in IterAllObjects(doc):
            for track in obj.GetCTracks():
                yield track
    
    def GetActiveTracks(doc, timeline_bit=None, exclude_hidden=True):
        """Returns the active tracks based on `selection_bit`.
    
        timeline_bit NBIT Reference:
        https://developers.maxon.net/docs/Cinema4DPythonSDK/html/modules/c4d/C4DAtom/GeListNode/index.html?highlight=getnbit#GeListNode.GetNBit
    
        Timeline 1: c4d.NBIT_TL1_SELECT,
        Timeline 2: c4d.NBIT_TL2_SELECT,
        Timeline 3: c4d.NBIT_TL3_SELECT,
        Timeline 4: c4d.NBIT_TL4_SELECT,
    
        F-Curve 1: c4d.NBIT_TL1_FCSELECT,
        F-Curve 2: c4d.NBIT_TL2_FCSELECT,
        F-Curve 3: c4d.NBIT_TL3_FCSELECT,
        F-Curve 4: c4d.NBIT_TL4_FCSELECT
    
        Issues:
        * No way (that I'm aware of) to detect which timeline is active.
        * No way to determine whether in Timeline or FCurve mode.
        * Not aware of whether track is visible/hidden in Timeline or not.
        """
    
        if doc is None:
            return
    
        if timeline_bit is None:
            timeline_bit = GetFirstOpenTimeline()
            if timeline_bit is None:
                # Error: No open timelines.
                return
    
        active_tracks = []
    
        hidden_bit = None
        if exclude_hidden:
            hidden_bit = GetTimelineHiddenBit(timeline_bit)
            if hidden_bit is None:
                return
    
        for track in IterAllTracks(doc):
            if exclude_hidden and track.GetNBit(hidden_bit):
                continue
            
            # Is selected?
            if track.GetNBit(timeline_bit):
                active_tracks.append(track)
    
        return active_tracks
    
    def GetFirstOpenTimeline():
        """Returns the timeline selection bit for the first (by timeline number, not opening order) open timeline.
    
        As of 2018/09/11 There's no way to determine if a Timeline is the most recently used/selected so this is the best we have.
        Also, there's no way to determine whether you're in Timeline or F-Curve mode, so Timeline (2) is the only one that will
        return `TL#_SELECT2` which is the selection bit for F-Curve data.
    
        Note: should only be called from the Main thread (e.g. no expressions, no objects)
        """
    
        timeline_commands_and_bits = [
            (465001510, c4d.NBIT_TL1_SELECT), # Timeline... (TL that opens with Dopesheet command)
            (465001511, c4d.NBIT_TL2_SELECT2), # Timeline... (2) (TL that opens with F-Curve command, will assume to be in F-Curve mode, this could have unexpected results if user manually switched to Dopesheet mode)
            (465001512, c4d.NBIT_TL3_SELECT), # Timeline... (3)
            (465001513, c4d.NBIT_TL4_SELECT) # Timeline... (4)
        ]
    
        for command_id, selection_bit in timeline_commands_and_bits:
            # Is this command shown as Checked in the UI? Odds are, the Timeline is open and in-use by the user.
            if c4d.IsCommandChecked(command_id):
                return selection_bit
    
    def GetTimelineHiddenBit(selection_bit):
        """Gives you the appropriate visibility bit to use based on a given Timeline selection bit.
        """
    
        visibility_bit = None
    
        # Timeline 1
        if selection_bit in [c4d.NBIT_TL1_SELECT, c4d.NBIT_TL1_SELECT2, c4d.NBIT_TL1_FCSELECT]:
            visibility_bit = c4d.NBIT_TL1_HIDE
        # Timeline 2
        elif selection_bit in [c4d.NBIT_TL2_SELECT, c4d.NBIT_TL2_SELECT2, c4d.NBIT_TL2_FCSELECT]:
            visibility_bit = c4d.NBIT_TL2_HIDE
        # Timeline 3
        elif selection_bit in [c4d.NBIT_TL3_SELECT, c4d.NBIT_TL3_SELECT2, c4d.NBIT_TL3_FCSELECT]:
            visibility_bit = c4d.NBIT_TL3_HIDE
        # Timeline 4
        elif selection_bit in [c4d.NBIT_TL4_SELECT, c4d.NBIT_TL4_SELECT2, c4d.NBIT_TL4_FCSELECT]:
            visibility_bit = c4d.NBIT_TL4_HIDE
    
        return visibility_bit
    
    def GetActiveKeys(doc, timeline_bit=None, exclude_hidden=True):
        """Returns all active keys.
    
        timeline_bit: Should be and NBIT, or will be auto-detected if set to None.
        exclude_hidden: Won't return active keys if tracks were explicitly hidden. Note: passively hidden (e.g. not dragged into TL, or filtered) will
        still return their active keys. I don't think there's a way to return the keys actually visible to the user.
    
        NBIT Reference:
        https://developers.maxon.net/docs/Cinema4DPythonSDK/html/modules/c4d/C4DAtom/GeListNode/index.html?highlight=getnbit#GeListNode.GetNBit
    
        Timeline 1: c4d.NBIT_TL1_SELECT,
        Timeline 2: c4d.NBIT_TL2_SELECT,
        Timeline 3: c4d.NBIT_TL3_SELECT,
        Timeline 4: c4d.NBIT_TL4_SELECT,
    
        # Note: The _SELECT2 bits are for F-Curve key selections, Don't use c4d.NBIT_TL1_FCSELECT, that's only for Tracks.
        F-Curve 1: c4d.NBIT_TL1_SELECT2,
        F-Curve 2: c4d.NBIT_TL2_SELECT2,
        F-Curve 3: c4d.NBIT_TL3_SELECT2,
        F-Curve 4: c4d.NBIT_TL4_SELECT2
        """
    
        if timeline_bit is None:
            timeline_bit = GetFirstOpenTimeline()
            if timeline_bit is None:
                # Error: No open timelines.
                return
    
        hidden_bit = None
        if exclude_hidden:
            hidden_bit = GetTimelineHiddenBit(timeline_bit)
            if hidden_bit is None:
                # Error: No way to determine if an element is hidden or not.
                return
    
        active_keys = []
    
        for track in IterAllTracks(doc):
            if exclude_hidden:
                # Track is marked as hidden, we don't care about these keys
                # even if they are selected.
                if track.GetNBit(hidden_bit):
                    continue
    
            curve = track.GetCurve()
            if curve is None:
                continue
    
            for key_id in xrange(curve.GetKeyCount()):
                key = curve.GetKey(key_id)
                is_selected = key.GetNBit(timeline_bit)
                if (key is not None) and is_selected:
                    active_keys.append(key)
    
        return active_keys
    
    def main():
        """Print the frame and value of the currently selected keys to the Python Console.
        """
    
        print " "
        print " "
        print "Active Keys"
        print "==========="
    
        active_keys = GetActiveKeys(doc)
        if active_keys:
            for key in GetActiveKeys(doc, c4d.NBIT_TL1_SELECT):
                print key.GetTime().GetFrame(doc.GetFps()), ": ", key.GetValue()
        else:
            print "No active keys."
    
    if __name__=='__main__':
        main()
    
    posted in Cinema 4D SDK •
    RE: Phong Break Selection

    I've done this in the past by going through all edges in the object and comparing the normals of neighboring polygons. A bit ugly, but it works. There are some helper functions in the Neighbor class you might want to look at: https://developers.maxon.net/docs/Cinema4DPythonSDK/html/modules/c4d.utils/Neighbor/index.html

    posted in Cinema 4D SDK •
    RE: Distributing Python Plugins that have Dependencies

    @m_adam Thanks, I'll have a look at pip and/or an installer script. I'm a bit concerned about adding anything directly to the resource/modules/python/libs folder as there's a chance some other plugin or script will have installed/need a different version of the same library. But, I suppose that I can report this as an installation error to the user and let them sort out which is more important for them to have installed.

    I'll be sure to let you know if I come to a resolution.

    posted in Cinema 4D SDK •
    Selecting Objects in Order

    I encountered what I believed was a bug in doc.SetActiveObjects(object, c4d.SELECTION_ADD). The objects that I selected would return a different order than I expected when I used doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER|c4d.GETACTIVEOBJECTFLAGS_CHILDREN).


    Here's the bug report I wrote up:

    Issue

    doc.SetActiveObject() does not seem to track object selection order.

    To Reproduce

    1. Create a new project file.
    2. Add 3 cubes: A, B, C
    3. Select them in order: A, B, C
    4. Create a new script, and paste this in:
    """Name-en-US: Print and Reverse Selection
    Description-en-US: Prints the names of all objects in the order they were selected. Then attempts to reverse selection order.
    """
    
    import c4d
    from c4d import gui
    
    def main():
        # Get Active Objects
        active_objects = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER|c4d.GETACTIVEOBJECTFLAGS_CHILDREN)
        if not active_objects:
            return
        
        # Collect the names of all objects, deselect as you go.
        names = []
        for obj in active_objects:
            names.append(obj.GetName())
            doc.SetActiveObject(obj, c4d.SELECTION_SUB)
        
        # Print the name of the objects based on their initial selection order.
        print names
        
        # Reselect the objects in reverse order
        for obj in reversed(active_objects):
            doc.SetActiveObject(obj, c4d.SELECTION_ADD)
        
        # Let C4D know something has changed
        c4d.EventAdd()
    
    if __name__=='__main__':
        main()
    
    1. Run this script.

    The console prints: ['a', 'b', 'c']

    1. Run this script again.

    BUG: The console again prints: ['a', 'b', 'c']

    EXPECTED: The console prints the new reverse selection order. ['c', 'b', 'a']

    Reference

    Documentation

    • c4d.documents.BaseDocument — Cinema 4D SDK 21.026 documentation

    Related Posts

    • active Objects SELECTIONORDER | PluginCafé
    • Adding to document selection issue | PluginCafé
    • selection order and shift-select | PluginCafé

    Turns out, it isn't a bug, but is instead a documentation issue. By calling doc.GetActiveObject() after each selection (as described here) you can update the selection caches and the selection order will appropriately update.

        # Reselect the objects in reverse order
        for obj in reversed(active_objects):
            doc.SetActiveObject(obj, c4d.SELECTION_ADD)
            
            # Update selection caches, needed if you want to use SELECTIONORDER
            # Reference: https://plugincafe.maxon.net/topic/9307/12387_adding-to-document-selection-issue/3
            doc.GetActiveObject()
    

    Request

    Please add this information to the doc.SetActiveObject, doc.SetSelection, and doc.GetActiveObjects sections of the Python and C++ documentation, and mention it as a possible fix for the Shift Select bug which I've encountered previously.

    Thank you,

    Donovan

    posted in Cinema 4D SDK •

    Latest posts made by dskeithbuck

    RE: Modifying Scene On First Open or Before Save?

    @C4DS @kbar @ferdinand - Thank you so much for the in-depth responses.

    @kbar I'll see if I can get my employer to pony up for a license for Call Python as that seems like the simplest root. Although, this is potentially a light enough lift that I can finally make the transition into C++ development.

    @ferdinand Helpful to know that I won't be able to interrupt the save process (probably a good thing from the user POV).

    posted in Cinema 4D SDK •
    Modifying Scene On First Open or Before Save?

    Hi!

    Is there a method to easily detect when a project is first created and then modify the document? Same for the moment just after the user has invoked the Save Command but before the document has actually saved?

    I'm hoping to apply custom settings on load (tweaking color parameters), and to perform a sanity check on the scene before saving (and possibly interrupt the save process if errors/issues are found).

    I'm thinking that some sort of MessageData plugin is the ticket, but I didn't find a message that is called on scene load/save.

    Other than that, I'm thinking that we can write custom scripts for loading/saving documents but I don't love that as it will require modifying keyboard shortcuts and still won't fix issues when someone drag & drops in assets to open them.

    Some sort of Node Data object might help w/ the saving/loading but we'd have to manually add it to projects.

    Thanks!

    Possibly Related?

    • Script Write to Annotation Tag on Save | PluginCafé
    • c4d.plugins.SceneSaverData — Cinema 4D SDK 24.111 documentation
    • Script Write to Annotation Tag on Save - Plugin Cafe Forums
    • SceneHookData Class Reference : Cinema 4D C++ SDK
    • Save Project As... new project name | PluginCafé
    posted in Cinema 4D SDK •
    RE: Left-Hand Local Coordinates to Right-Hand Local Coordinates?

    Thank you @zipit and @m_magalhaes for your replies! @zipit your response put me on the right track. The issue was that I was trying to do all of the conversions using C4D's left-handed coordinate matrices, and it was auto-fixing my "malformed" matrices.

    I ended up porting the Three.js Matrix4 and Euler classes from JS to python and using them in my script.

    So, the final code (leaving out the ported methods) looked something like:

            mg = self.op.GetMg()
            parent_mg = self.op.GetUpMg()
    
            c4d_to_three = c4d.Matrix(
                off=c4d.Vector(0),
                v1=c4d.Vector(1, 0, 0),
                v2=c4d.Vector(0, 1, 0),
                v3=c4d.Vector(0, 0, -1)
            )
    
            # Convert to new coordinate space
            # http://www.techart3d.com/2016/02/convert-left-handed-to-right-handed-coordinates/
    
            mg_three_coords = c4d_to_three * mg * c4d_to_three
            parent_mg_three_coords = c4d_to_three * parent_mg * c4d_to_three
    
            mg_mat4 = Matrix4(mg_three_coords)
            parent_mg_mat4 = Matrix4(parent_mg_three_coords)
    
            inv_parent_mg_mat4 = parent_mg_mat4.Clone()
            inv_parent_mg_mat4 = inv_parent_mg_mat4.Inverse()
    
            node_local = inv_parent_mg_mat4.Clone()
            node_local = node_local.MultiplyMatrices(inv_parent_mg_mat4, mg_mat4)
    
            position, scale, rotation = node_local.Decompose()
    
            if position != c4d.Vector(0):
                self.props["position"] = position
    
            if scale != c4d.Vector(1):
                self.props["scale"] = scale
    
            if rotation != c4d.Vector(0):
                self.props["rotation"] = rotation
    
    posted in General Talk •
    Copying All Compatible Properties from One Object to Another?

    Hi,

    Is there a preferred way of copying all compatible object properties from an object of one type, to an object of another type? Things, I'm thinking of:

    • Name
    • Matrix
    • Layers
    • Visibility
    • Tags
    • Animation
    • Compatible Properties / Animation - So probably, PSR, but maybe also Visibility or Time Tracks, etc.

    I think I know how to approach this manually, but don't want to duplicate efforts if there's an API method, or existing snippet that does this as I'm afraid of leaving out something critical, and/or future proofing against future API additions.
    Thanks!

    Donovan

    posted in Cinema 4D SDK •
    RE: Best Practices for Reporting Errors to Users?

    @s_bach & @zipit - Thank you both for the thoughtful replies!

    I think I'll opt for the status bar for most user-facing feedback, and console logging for anything bigger. Watching the "What could possibly go wrong?" talk now.

    posted in Cinema 4D SDK •
    Best Practices for Reporting Errors to Users?

    Hi,

    I've been seeing a lot more usage of code like raise ValueError("Invalid path selected") in the Python examples. As I understand it, these errors tend to get surfaced in the Console - which is useful to developers, but less useful to Users.

    Here's a simple script that asks a user to select an image in a folder containing multiple images.

    import c4d
    
    def get_images_path():
        image_path = c4d.storage.LoadDialog(
            type=c4d.FILESELECTTYPE_IMAGES,
            title="Select an image in the folder you want to bulk rename",
            flags=c4d.FILESELECT_LOAD
        )
        
        if not image_path:
            raise ValueError("Valid image path not selected.")
        
        return image_path
    
    def main():
        images_path = get_images_path()
        
        c4d.gui.MessageDialog(images_path)
    
    if __name__=='__main__':
        main()
    

    How should I gracefuly accept a user hitting "Cancel"? How should I provide the user with a helpful error if they've selected the wrong filetype (let's for example pretend I'm looking for .png files rather than misc image formats)?

    Should we use popup messages? Status bar updates? Console prints?

    Do I need to do something like:

    try:
        images_path = get_images_path()
    except ValueError as error:
        c4d.gui.MessageDialog(error)
    

    ...every time I call any method that might throw an error? Or is there a way to auto-promote exceptions/errors as alert dialogs/status updates?
    Thanks,

    Donovan

    posted in Cinema 4D SDK •
    Selecting Objects in Order

    I encountered what I believed was a bug in doc.SetActiveObjects(object, c4d.SELECTION_ADD). The objects that I selected would return a different order than I expected when I used doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER|c4d.GETACTIVEOBJECTFLAGS_CHILDREN).


    Here's the bug report I wrote up:

    Issue

    doc.SetActiveObject() does not seem to track object selection order.

    To Reproduce

    1. Create a new project file.
    2. Add 3 cubes: A, B, C
    3. Select them in order: A, B, C
    4. Create a new script, and paste this in:
    """Name-en-US: Print and Reverse Selection
    Description-en-US: Prints the names of all objects in the order they were selected. Then attempts to reverse selection order.
    """
    
    import c4d
    from c4d import gui
    
    def main():
        # Get Active Objects
        active_objects = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER|c4d.GETACTIVEOBJECTFLAGS_CHILDREN)
        if not active_objects:
            return
        
        # Collect the names of all objects, deselect as you go.
        names = []
        for obj in active_objects:
            names.append(obj.GetName())
            doc.SetActiveObject(obj, c4d.SELECTION_SUB)
        
        # Print the name of the objects based on their initial selection order.
        print names
        
        # Reselect the objects in reverse order
        for obj in reversed(active_objects):
            doc.SetActiveObject(obj, c4d.SELECTION_ADD)
        
        # Let C4D know something has changed
        c4d.EventAdd()
    
    if __name__=='__main__':
        main()
    
    1. Run this script.

    The console prints: ['a', 'b', 'c']

    1. Run this script again.

    BUG: The console again prints: ['a', 'b', 'c']

    EXPECTED: The console prints the new reverse selection order. ['c', 'b', 'a']

    Reference

    Documentation

    • c4d.documents.BaseDocument — Cinema 4D SDK 21.026 documentation

    Related Posts

    • active Objects SELECTIONORDER | PluginCafé
    • Adding to document selection issue | PluginCafé
    • selection order and shift-select | PluginCafé

    Turns out, it isn't a bug, but is instead a documentation issue. By calling doc.GetActiveObject() after each selection (as described here) you can update the selection caches and the selection order will appropriately update.

        # Reselect the objects in reverse order
        for obj in reversed(active_objects):
            doc.SetActiveObject(obj, c4d.SELECTION_ADD)
            
            # Update selection caches, needed if you want to use SELECTIONORDER
            # Reference: https://plugincafe.maxon.net/topic/9307/12387_adding-to-document-selection-issue/3
            doc.GetActiveObject()
    

    Request

    Please add this information to the doc.SetActiveObject, doc.SetSelection, and doc.GetActiveObjects sections of the Python and C++ documentation, and mention it as a possible fix for the Shift Select bug which I've encountered previously.

    Thank you,

    Donovan

    posted in Cinema 4D SDK •
    RE: Styling BitmapButtons

    @zipit said in Styling BitmapButtons:

    While I cannot say much about your problem, here are some bits that you might (or might not) find useful.

    Thank you for taking the time to respond!

    1. Have you tried / is it possible for your task to define your dialog resource as a file system rather than by code?

    My interface includes a large list of dynamic elements. I'm not sure that I'll be able to give each a unique ID unless I create them with code. But I'll see if I can restructure at least some of this.

    Although I cannot find the exact passage right now, it says somewhere in the docs, that you should use files, since it is the only way to get all features, except for menus, which are only constructable by code.

    Gotcha. Good to know. The docs have really improved, but there's lots of little tidbits like this that you'll only see if you read every page of every parent class rather than seeing all of the relevant info in the method definition.

    1. I came to the (unconfirmed) conclusion, that some of the more exotic CustomGui data types are apparently not fully supported for dialog resources (the same definition for a SPLINE gadget worked just fine in a description resource) and wrote my own little thing.

    Ah. That makes sense as well. I think for now I'll live with the stylistic limitations of the current BitmapButton as it's easier than writing my own.
    Thanks!

    Donovan

    posted in Cinema 4D SDK •
    Styling BitmapButtons

    Hi,

    I'm hoping to add some Bitmap Buttons to a project I'm working on, but I'm having difficulty styling them (and initially: creating them).

        def AddIconButton(self, button_id, title, icon_id, tooltip, border=c4d.BORDER_ROUND):
            """
            Reference: https://plugincafe.maxon.net/topic/11462/understanding-setcommanddragid/2
            """
    
            bc = c4d.BaseContainer()
            bc.SetBool(c4d.BITMAPBUTTON_BUTTON, True)
            bc.SetInt32(c4d.BITMAPBUTTON_ICONID1, icon_id)
            bc.SetInt32(c4d.BITMAPBUTTON_OUTBORDER, border)
            bc.SetString(c4d.BITMAPBUTTON_TOOLTIP, tooltip)
            bc.SetString(c4d.BITMAPBUTTON_STRING, title)
            icon_width_height = 24
            bc.SetInt32(c4d.BITMAPBUTTON_FORCE_SIZE, icon_width_height)
            button = self.AddCustomGui(button_id, c4d.CUSTOMGUI_BITMAPBUTTON, title, c4d.BFH_LEFT, 0, 0, bc)
    
            return button
    

    A lot of the styling flags seem to have no impact. Specifically:

    • BITMAPBUTTON_BORDER
    • BITMAPBUTTON_STRING

    I created some simple test code to show each of the styles and the buttons looked identical:

    
            bitmapbutton_border_styles = [("BORDER_NONE", c4d.BORDER_NONE),
            ("BORDER_THIN_IN", c4d.BORDER_THIN_IN),
            ("BORDER_THIN_OUT", c4d.BORDER_THIN_OUT),
            ("BORDER_IN", c4d.BORDER_IN),
            ("BORDER_OUT", c4d.BORDER_OUT),
            ("BORDER_GROUP_IN", c4d.BORDER_GROUP_IN),
            ("BORDER_GROUP_OUT", c4d.BORDER_GROUP_OUT),
            ("BORDER_OUT2", c4d.BORDER_OUT2),
            ("BORDER_OUT3", c4d.BORDER_OUT3),
            ("BORDER_BLACK", c4d.BORDER_BLACK),
            ("BORDER_ACTIVE_1", c4d.BORDER_ACTIVE_1),
            ("BORDER_ACTIVE_2", c4d.BORDER_ACTIVE_2),
            ("BORDER_ACTIVE_3", c4d.BORDER_ACTIVE_3),
            ("BORDER_ACTIVE_4", c4d.BORDER_ACTIVE_4),
            ("BORDER_GROUP_TOP", c4d.BORDER_GROUP_TOP),
            ("BORDER_ROUND", c4d.BORDER_ROUND),
            ("BORDER_SCHEME_EDIT", c4d.BORDER_SCHEME_EDIT),
            ("BORDER_SCHEME_EDIT_NUMERIC", c4d.BORDER_SCHEME_EDIT_NUMERIC),
            ("BORDER_OUT3l", c4d.BORDER_OUT3l),
            ("BORDER_OUT3r", c4d.BORDER_OUT3r),
            ("BORDER_THIN_INb", c4d.BORDER_THIN_INb),
            ("BORDER_MASK", c4d.BORDER_MASK),
            ("BORDER_TEXT_DOTTED", c4d.BORDER_TEXT_DOTTED),
            ("BORDER_WITH_TITLE_MONO", c4d.BORDER_WITH_TITLE_MONO),
            ("BORDER_WITH_TITLE_BOLD", c4d.BORDER_WITH_TITLE_BOLD),
            ("BORDER_WITH_TITLE", c4d.BORDER_WITH_TITLE)]
    
            for border_id_name, border_id in bitmapbutton_border_styles:
                self.AddIconButton(0, border_id_name, ICON_NEW_PROJECT, border_id_name, border=border_id)
    

    Ideally, I'd love a button that matches the look/feel of docked icons w/ text:
    69f1aaa2-626f-4699-9e3e-ef2e4290bff1-image.png

    Any suggestions?

    Also, FWIW, some sample code for how to add BitmapButtons inside of the BitmapButton class would be really helpful. I struggled for quite a while trying to create a button using c4d.gui.BitmapButtonCustomGui() but was stumped when I couldn't figure out how to add it to a dialog.
    Thank you,

    Donovan

    posted in Cinema 4D SDK •
    RE: Distributing Python Plugins that have Dependencies

    @m_adam said in Distributing Python Plugins that have Dependencies:

    As an Idea but didn't try and will not have the time to do it today (so if you try, do it at your own risks), but you could try to install things using directly the python executable from the resource folder

    I'm moving onto different parts of development, but I'll likely want to investigate this later and will post an update if I do. Thanks!

    posted in Cinema 4D SDK •