Snap settings issues in R21/R23?



  • Hello; here's a collection of more questions / remarks about the Snap module...

    (1) The following symbols seem to be obsolete (???):
    SNAPMODE_EDGEMID (always True)
    SNAPMODE_GUIDEMID (always False)
    SNAPMODE_POLYGONCENTER (always True)
    SNAPMODE_SPLINEMID (always False)
    They have no corresponding setting in the GUI, where we instead find SNAPMODE_MIDPOINT which works for all snapping modes, so I would assume a combination of e.g. SNAPMODE_POLYGON and SNAPMODE_MIDPOINT should be used instead of SNAPMODE_POLYGONCENTER. (At first, I thought that the MID modes were binary-or-combinations of SNAPMODE_MIDPOINT with one of the other modes, but on checking the numeric values of the modes, they don't appear to be part of a bitset.) (OTOH, those four constants have been in the docs for a long time and are still in both the Python and C++ docs... so, is there a hidden meaning?)

    (2) The function c4d.modules.snap.SetSnapSettings(doc, bc, snapmode=NOTOK) has this very mysterious parameter snapmode. According to the docs, "Optionally specify the snap mode to set the settings for instead of the global settings." This text suggests that each snapmode has a local setting of parameters which are applied if that snapmode is used. -- This is not the same as the "Tool Specific" checkbox, which creates local settings for tools (e.g. Move Tool), not for snapmodes!

    However: It doesn't work that way. Using something else for snapmode than NOTOK results in that snapmode being activated, and the parameters being set (at least in the R21, in the R23 only the mode is activated), but there is no indication of any "locality" of these settings. The GUI has no option for snapmode-local settings either.

    It also makes no sense because multiple snapmodes can be active at the same time - which parameter set would be used then? (Maybe it's a relic from an earlier C4D version...?)

    (3) Controlling the checkbox "Tool Specific" through SetSnapSettings doesn't work:
    bc[c4d.SNAP_SETTINGS_TOOL] = True has no effect in either R21 or R23.1. (bc being the base container argument.)
    The documentation claims it does. Seems like a bug to me?

    (4) And, of course: The mesh check constants are not mentioned at all in the Snap settings page. You can find them on one of the auto-generated "constant" pages:
    https://developers.maxon.net/docs/Cinema4DPythonSDK/html/classic_resource/unknown/dmodeling.html?highlight=mesh_check_enabled
    but the snap overview ignores them. They can be set by SetSnapSettings so I believe they should be documented there as well.

    (5) Finally: The C++ documentation mentions "After changing the snap settings, call SpecialEventAdd() with ID 440000118 for the snap system to recognize the change." This is not in the Python doc for SetSnapSettings, so I assume it's not needed there and the Python wrapper already takes care of that call?

    Thanks for insights... 😁



  • Hi @Cairyn,

    thank you for reaching out to us, I'll answer in bullet points to keep things short:

    1. [...] so, is there a hidden meaning?

    Not really, these seem to have been mostly deprecated. But unlike the QUANTIZE_GRID symbol, these do pop up in some places in Cinema's internal API. But there is no functionality attached to them anymore as far as I can see. It is just some GUI and data (description stuff) which is probably just dormant.

    That is probably why they won't be removed from the API very soon and subsequently probably also not from the C++ docs. But I do not see any reason to keep them in the Python docs.

    1. However: It doesn't work that way [...]

    Each BaseDocument carries snap settings in its modelling settings.

    bc = doc.GetSettingsInstance(c4d.DOCUMENTSETTINGS_MODELING)
    for cid, val in bc[c4d.SNAP_SETTINGS]:
        print (cid, val)
    

    But there are also multiple BasePlugin for the different snap modes/tools, which obviously also carry these snap settings. When you pass NOTOK, Cinema is trying to copy some of your data (passed as the argument bc) into the plugin containers. But in both cases it will write at the end into the document container.

    In the end this seems to be a rather exotic functionality that has very little practical usage for the end user, as the only data, that is being modified when going over the plugins in case of snampmode == NOTOK, is SNAP_SETTINGS_ENABLED - which is written from you passed data to the plugins. There are also other differences, like copying or referencing the passed data - in case of NOTOK they will be copied.

    1. Controlling the checkbox "Tool Specific" through SetSnapSettings doesn't work.

    What are you passing as snampmode in this case?

    1. The mesh check constants are not mentioned at all in the Snap settings page.

    I am not quite sure why you would expect them to appear in the context of the snap core? These are the mesh validity symbols used in Attribute_Manager-> Mode-> Modelling-> Mesh_Checking - assuming you are referring to symbols like MESH_CHECK_POINT. Could you please elaborate?

    1. [...] so I assume it's not needed there and the Python wrapper already takes care of that call?

    No, it is not doing that for you. I currently cannot see any other reason than it being forgotten to be mentioned for now.

    Cheers,
    Ferdinand



  • @zipit Thanks for looking at this.

    (1) ok - I haven't consulted version documentations before R21, so I don't know how the snap settings used to work in prehistoric times. I'm going to ignore these constants.

    (2) hmm, this explanation is hard to understand without looking at the underlying code, and I don't quite see the connection to the explanation in the docs either. The BaseDocument is already another parameter in the function call for SetSnapSettings, so it's clear that this container is a valid target for the settings. We also have the "Tool Specific" flag that allows us to set snap settings per tool, so these would have their own snap settings container - however, the parameter snapmode defines a snapmode and not a tool.

    (I would be less surprised if snapmode was a tool ID, like a call SetSnapSettings(doc,bc,c4d.ID_MODELING_MOVE) which I could identify as "set these snap settings as local parameters when the Move tool is active, so this is used when "Tool Specific" is active. But ID_MODELING_MOVE is not a snapmode! -- I even tried that in spite of the documentation, but it doesn't do anything.)

    From a practical standpoint, if I execute the following script in R23.1:

    import c4d
    from c4d import gui, utils, modules
    from c4d.modules import snap
    
    def main():
        bc = c4d.BaseContainer()
        bc[c4d.SNAP_SETTINGS_ENABLED] = True
        bc[c4d.SNAP_SETTINGS_RADIUS] = 15
        snap.SetSnapSettings(doc, bc, c4d.SNAPMODE_POINT)
        c4d.EventAdd()
    
    if __name__=='__main__':
        main()
    

    then the Vertex checkbox will be checked, and all the other parameters will be ignored (enabled and radius). I see no way that the GUI (can't look at the underlying system, of course) would use these settings in any form.

    Maybe that parameter is used internally for very specific purposes, but from a user's perspective, the explanation given in the docs for the parameter snapmode is plain wrong and suggests a behavior that doesn't happen.

    (3) The simplest of all settings (R23.1):

        bc = c4d.BaseContainer()
        bc[c4d.SNAP_SETTINGS_ENABLED] = True
        bc[c4d.SNAP_SETTINGS_TOOL] = True
        snap.SetSnapSettings(doc, bc)
    

    The "Enable Snapping" checkbox gets checked; the "Tool Specific" checkbox does not. The same happens if I explicitly set the parameter NOTOK. All other snapmodes, like SNAPMODE_POINT, completely ignore the BaseContainer anyway, as mentioned in point (2).
    Meanwhile (after your reply) I tested whether calling c4d.SpecialEventAdd(440000118) would make a difference, but it doesn't.

    Using EnableSnap doesn't work either, I didn't expect it to but just for completeness' sake:

        snap.EnableSnap(True, doc, c4d.SNAP_SETTINGS_TOOL)
        c4d.SpecialEventAdd(440000118)
        c4d.EventAdd()
    

    I have not yet tried to modify the document's GetSettingsInstance() container directly, but all other tests simply don't acknowledge the SNAP_SETTINGS_TOOL.

    (4) The snap core, as documented in the c4d.modules.snap documentation, covers four functional areas: snap, quantize, mesh check, and workplane. Three of them are mentioned in the class docs: snap, quantize, and workplane. Mesh check is ignored.
    Granted, mesh check does not have any special unique functions in this class, but it uses SetSnapSettings to set its parameters:

    import c4d
    from c4d import gui, utils, modules
    from c4d.modules import snap
    
    def main():
        bc = c4d.BaseContainer()
        bc[c4d.MESH_CHECK_ENABLED] = True
        bc[c4d.MESH_CHECK_POINT] = True
        bc[c4d.MESH_CHECK_EDGEPOINT_THRESHOLD] = utils.DegToRad(90.0)
        bc[c4d.MESH_CHECK_EDGEPOINT] = True
        snap.SetSnapSettings(doc, bc)
        c4d.EventAdd()
    
    if __name__=='__main__':
        main()
    

    so I would expect the constants to be listed there. The quantize constants are!

    In fact, I remember the topic from some forum a few years ago: Before you (Maxon) started to list the constants from header files automatically, the mesh check constants and their relationship to the snap core weren't listed in the docs at all. Since the R23 docs, you can at least find the constants, and logically derive how they may be set practically. Nevertheless, I would expect them to be listed under SetSnapSettings like all others that belong to the snap core.

    Speaking of constants, since the R23 it is also possible to set the snapmode checkboxes through SetSnapSettings:
    bc[c4d.SNAPMODE_POINT] = True (which hard crashes C4D in R21, btw) instead of using EnableSnap. This has also not made its way into the docs (or maybe that wasn't intended to work?).

    (5) Okay, I will use that call in addition. But I don't see what that function does there... According to the Customize Command window, 440000118 belongs to "Snap Radius Slider" with the description "Icon Palette GUI to control the screen space radius of snapping". Does this constant have a different meaning when I use it in SpecialEventAdd()? What is C4D doing internally?

    Also, while checking this, I tried to add the Snap Radius Slider to a toolbar that I added to the Attribute Manager:

    8773fa44-ab0d-42a8-9f89-321b51eb6c9f-image.png

    As you can see, this Snap Radius does not follow the Snap Radius in the Attribute Manager, and vice versa. Is that not the same setting? Is that a bug?



  • Hi @Cairyn,

    I do not have time to answer all your points today and I would probably also would have to look at the code again for untangling all the details there, but I feel there is at least a minor misconception of yours. When invoking SetSnapSettings with a snapmode other than the default, i.e. NOTOK, all the function is doing, is writing to the data container of the document passed as doc. In the document container there are multiple sub-containers in the snap container for the snap modes. See the little script at the end for clarification.

    As already stated in my previous posting, I do not think that there is much to gain for the end user with snapmode. It probably should be marked as private.

    Cheers,
    Ferdinand

    import c4d
    from c4d.modules import snap
    
    def peek_document_snap_settings(doc):
        """Peeks into the snap settings of the document.
        """
        bc = doc.GetSettingsInstance(c4d.DOCUMENTSETTINGS_MODELING)
        
        snap = bc[c4d.SNAP_SETTINGS]
        print ("Global - Enabled", snap[c4d.SNAP_SETTINGS_ENABLED])
        print ("Global - Radius", snap[c4d.SNAP_SETTINGS_RADIUS])
        snap = bc[c4d.SNAP_SETTINGS][c4d.SNAPMODE_POINT]
        print ("Point - Enabled", snap[c4d.SNAP_SETTINGS_ENABLED])
        print ("Point - Radius", snap[c4d.SNAP_SETTINGS_RADIUS])
        print ("-" * 100)
    
    def main():
        """Writes both to the point and global snapping container.
        """
        peek_document_snap_settings(doc)
        
        # Write to the point specific settings. This will only be reflected in
        # container of the document.
        data = c4d.BaseContainer()
        data[c4d.SNAP_SETTINGS_ENABLED] = True
        data[c4d.SNAP_SETTINGS_RADIUS] = 25
        snap.SetSnapSettings(doc, data, c4d.SNAPMODE_POINT)
        
        # Write to the global settings. This will be reflected both in the GUI
        # as well as the document container.
        data = c4d.BaseContainer()
        data[c4d.SNAP_SETTINGS_ENABLED] = True
        data[c4d.SNAP_SETTINGS_RADIUS] = 50
        snap.SetSnapSettings(doc, data)
    
        c4d.EventAdd()
        peek_document_snap_settings(doc)
    
    if __name__=='__main__':
        main()
    


  • @zipit Hello, thanks for the code. I see now how the containers are arranged, but the question remains why these subcontainers are even there in the first place. After all, the GUI doesn't indicate any of this, so maybe it's just an internal help / cache / convenience thing?

    Let's assume that we have sub-containers with different radii for vertex and polygon snapping. I can imagine that internally each snapping works independently and uses its own radius from its specific container, and shows the snap only when applicable. But the user wouldn't know and has no way of setting it with the current GUI, which only has one radius setting that doesn't change with the selection of the snapmode. By script, it could be set in the subcontainer, but is it actually used that way?

    Even more interesting: SNAPMODE_MIDPOINT is nominally a snapmode by itself. But actually, it is a modifier for the snapmodes edge, polygon, spline, and guide. With the subcontainers, we could have an edge snap active where midpoint is set, and a polygon snap as well where midpoint is not set, so the interactive snapping would consider edge mid points but not polygon centers. That actually makes sense -- however, midpoint is a snapmode which therefore has its own subcontainer(???) and not a setting for the other snapmodes.

    I have the feeling that the subcontainers for snapping is actually a concept that just didn't make it into the GUI design, and midpoint (as well as some other snapmodes maybe) is wrongly classified...

    I need to try the subcontainers now to test whether the snapping is actually (notably) influenced by them. Which would raise the question why you can't see any of that in the GUI.



  • okay, I just cobbled together a test.
    The calls to SetSnapSettings actually do write into the subcontainers:

    import c4d
    from c4d import gui, utils, modules
    from c4d.modules import snap
    
    def peek_document_snap_settings(doc):
        """Peeks into the snap settings of the document.
        """
        bc = doc.GetSettingsInstance(c4d.DOCUMENTSETTINGS_MODELING)
        
        snap = bc[c4d.SNAP_SETTINGS]
        print ("Global - Enabled", snap[c4d.SNAP_SETTINGS_ENABLED])
        print ("Global - Radius", snap[c4d.SNAP_SETTINGS_RADIUS])
        print ("Global - Mid Point", snap[c4d.SNAPMODE_MIDPOINT]) # subcontainer
        snap = bc[c4d.SNAP_SETTINGS][c4d.SNAPMODE_POINT]
        print ("Point - Enabled", snap[c4d.SNAP_SETTINGS_ENABLED])
        print ("Point - Radius", snap[c4d.SNAP_SETTINGS_RADIUS])
        print ("Point - Mid Point", snap[c4d.SNAPMODE_MIDPOINT])
        snap = bc[c4d.SNAP_SETTINGS][c4d.SNAPMODE_EDGE]
        print ("Edge - Enabled", snap[c4d.SNAP_SETTINGS_ENABLED])
        print ("Edge - Radius", snap[c4d.SNAP_SETTINGS_RADIUS])
        print ("Edge - Mid Point", snap[c4d.SNAPMODE_MIDPOINT])
        snap = bc[c4d.SNAP_SETTINGS][c4d.SNAPMODE_POLYGON]
        print ("Polygon - Enabled", snap[c4d.SNAP_SETTINGS_ENABLED])
        print ("Polygon - Radius", snap[c4d.SNAP_SETTINGS_RADIUS])
        print ("Polygon - Mid Point", snap[c4d.SNAPMODE_MIDPOINT])
    
        print ("-" * 100)
    
    def main():
        bc = c4d.BaseContainer()
        bc[c4d.SNAP_SETTINGS_ENABLED] = True
        bc[c4d.SNAP_SETTINGS_RADIUS] = 40
        snap.SetSnapSettings(doc, bc, c4d.SNAPMODE_POINT)
    
        bc = c4d.BaseContainer()
        bc[c4d.SNAP_SETTINGS_ENABLED] = True
        bc[c4d.SNAP_SETTINGS_RADIUS] = 5
        bc[c4d.SNAPMODE_MIDPOINT] = True
        snap.SetSnapSettings(doc, bc, c4d.SNAPMODE_EDGE)
    
        bc = c4d.BaseContainer()
        bc[c4d.SNAP_SETTINGS_ENABLED] = True
        bc[c4d.SNAP_SETTINGS_RADIUS] = 5
        bc[c4d.SNAPMODE_MIDPOINT] = False
        snap.SetSnapSettings(doc, bc, c4d.SNAPMODE_POLYGON)
    
        c4d.EventAdd()
        
        peek_document_snap_settings(doc)
    
    if __name__=='__main__':
        main()
    

    However, these values don't have any effect on the behavior of the snapping in the viewport. Here, only the global radius and enablement is used, and midpoint is a subcontainer by itself anyway. Setting midpoint in the subcontainers is probably the wrong way to go anyway because it's a snapmode by itself, but it's worth a try...

    Which raises the question (again, or still): Are these subcontainers even used at all by the snapping system? Or is that a preparation for future things to come / sign of an abandoned concept? You already said it's probably of no use to the end user (and rightfully so, as the GUI doesn't offer any possibility to use it gainfully), but is there code in C4D itself where it comes to bear...?

    Perhaps in snapmodes defined by my own C++ plugins, which could have their own GUI and their own subcontainers. You mentioned BasePlugin before...



  • Hi @Cairyn,

    yes, writing snap tolerances to these tool specific containers is not being respected. And after some further investigations I have come to the conclusion, that most things written to document snap settings (even the global settings) will be ignored. A workaround, which oddly is also done by Cinema internally, is to just invoke the respective plugin command for the snap implementations. I will have to ask the modelling team on Monday, why things are implemented this way and subsequently why they did choose to keep the snap settings around in this manner.

    In the mean time please take a look at the script below, which shows you how to read and write to SNAP_SETTINGS_TOOL with this rather odd approach.

    Cheers,
    Ferdinand

    import c4d
     
    def main():
        """How to weasel your way around SNAP_SETTINGS_TOOL. 
    
        The same approach can be applied to other snap tools (basically 
        everything you can turn on or off in the snapping tool bar).
        """
        # Get the command state.
        state = c4d.IsCommandChecked((c4d.SNAP_SETTINGS_TOOL))
        print ("Tool Specific is enabled:", state)
    
        # Toggle the command.
        c4d.CallCommand(c4d.SNAP_SETTINGS_TOOL)
    
        # Get the new command state.
        state = c4d.IsCommandChecked((c4d.SNAP_SETTINGS_TOOL))
        print ("Tool Specific is enabled:", state)
        
    if __name__ == "__main__":
        main()
    


  • Hi,

    without further feedback, we will consider this thread as solved by Monday and flag it accordingly.

    Cheers,
    Ferdinand



  • @zipit said in Snap settings issues in R21/R23?:

    I will have to ask the modelling team on Monday, why things are implemented this way and subsequently why they did choose to keep the snap settings around in this manner.

    I was just waiting whether the modeling team had any comments on that...