SOLVED Snap settings issues in R21/R23?

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

Hi @Cairyn,

I am so sorry, I totally forgot about this. I am on it.

Cheers,
Ferdinand

Hi @Cairyn,

sorry again for the long wait, I have asked the devs and:

  1. The functionality of snapmode has never been used and has no effect at all at the moment. It will probably be marked as deprecated in an upcoming release.
  2. The approach shown by me is a valid approach. Alternatively you can also write to and read from the active tool settings (see example below).

Cheers,
Ferdinand

import c4d

def main():
    """
    """
    # Instance of the active tool's settings container.
    bc = doc.GetActiveToolData()
    if not isinstance(bc, c4d.BaseContainer):
        raise MemoryError()

    # Get a copy of the tool's snap settings or create a container when
    # there has no snap settings been created yet.
    snap = bc.GetContainer(c4d.SNAP_SETTINGS) or c4d.BaseContainer()

    # Write some data for that tool, specifically set the tool specific snap radius.
    snap[c4d.SNAP_SETTINGS_TOOL] = True
    snap[c4d.SNAP_SETTINGS_RADIUS] = 50.

    # Write the snap settings back to the tool.
    bc.SetContainer(c4d.SNAP_SETTINGS, snap)
    c4d.EventAdd()

if __name__=='__main__':
    main()

@zipit Thanks, I thought so... but you never know whether some functionality has a hidden internal purpose. Deprecating it will probably be the best solution.