Solved Force VertexMap display

I asked this a few years ago (six years ago, actually) but I ask again, because there is a solution now (I hope).
Is there any way, in Python, to force the display in the editor of the strength of a Vertex Map, actually, showing what would be shown if a Vertex Map was active, but without having to click the Vertex Map itself?

Hello @rui_mac,

So, I had a look at your code. It was unfortunately not executable in this form, even when I filled in some gaps like adding the necessary resources or re-indenting stuff. So, I cannot say many concrete things here, and only look at the general form.

I must also point out again, that this is a hack and not officially supported. If you chose to take this route instead of drawing the data yourself, possible side effects will be up to you to handle.

  • A general thing is that you seem to assume that Message, Init, and other methods to always run on the main thread. But there is no guarantee for that; when you want to carry out actions only allowed on the main thread, you must still check for being on the main thread. I however still think that adding a tag from a non-main thread was not your issue here.
  • This line in your Init is dangerous and might be the source for your problems, as you forcibly select the host object of your tag: op.SetBit(c4d.BIT_ACTIVE)
  • In general, you are also very generous with Message(c4d.MSG_UPDATE) calls. They are first of all here unnecessary for the flag changes and secondly also dangerous. I would never send messages to a node which is already part of the scene graph from threaded environment (you call op.Message(c4d.MSG_UPDATE) in multiple contexts).
  • Using the name of a node is a poor way of identifying a node, you should use UUIDs in Python.
  • I have provided below a TagData solution for your problem. This is quite a bit of busy work, especially when you want to properly clean up after yourself. This works fine for me.

if it would be possible to make the VertexMap shading appear, simply by using this visible VertexMap tag, I would be more than fine with this.

It would be the same, just iterate over the tags of the node hosting your tag and select whatever tags you want to select.

Cheers,
Ferdinand

The result:
9969441c-0ebc-4bca-a376-a693f5795e07-image.png
The plugin: pc14184.zip

The pyp file of the plugin:

"""Implements a tag which manages a hidden vertex map to draw data.
"""
import c4d
import typing

class VertexMapMixData(c4d.plugins.TagData):
    """Implements a tag which manages a hidden vertex map to draw data.
    """
    PLUGIN_ID: int = 1037312
    DRAWTAG_ID: int = 1060040

    PLUGIN_NAME: str = "VertexMapMix"
    PLUGIN_DESCRIPTION: str = "Tvertexmapmix"

    def __init__(self) -> None:
        """Initializes the instance, specifically its _hiddenDrawTags lookup table.
        """
        self._hiddenDrawTags: dict[bytes: c4d.BaseTag] = {}
        super().__init__()

    def FlushHiddenTags(self, node: c4d.GeListNode) -> None:
        """Flushes the #_hiddenDrawTags cache and removes all the hidden tags from the hosting node.
        """
        host: typing.Optional[c4d.BaseObject] = node.GetMain()
        if not isinstance(host, c4d.BaseObject):
            return

        tag: c4d.BaseTag
        for tag in host.GetTags():
            if tag.GetType() != c4d.Tvertexmap:
                continue

            # This is a hidden drawing tag instance.
            uuid: typing.Optional[memoryview] = tag.FindUniqueID(VertexMapMixData.DRAWTAG_ID)
            if isinstance(uuid, memoryview):
                tag.Remove()

        self._hiddenDrawTags = {}

    def GetHiddenDrawTag(self, node: c4d.PolygonObject) -> typing.Optional[c4d.VariableTag]:
        """Retrieves the hidden drawing tag on a polygon object.
        """
        # Bail when #node is not a polygon object or not attached to a document.
        if (not isinstance(node, c4d.PolygonObject) or
                not isinstance(node.GetDocument(), c4d.documents.BaseDocument)):
            return None

        # Get the UUID of the polygon object so that we look up its hidden tag in the lookup.
        uuid: typing.Optional[memoryview] = node.FindUniqueID(c4d.MAXON_CREATOR_ID)
        if not isinstance(uuid, memoryview):
            return None

        # Check if there is already a cached hidden draw tag in dictionary of the plugin interface.
        hiddenTag: typing.Optional[c4d.BaseTag] = self._hiddenDrawTags.get(bytes(uuid), None)
        if isinstance(hiddenTag, c4d.BaseTag) and hiddenTag.IsAlive():
            return hiddenTag

        # Just for verbosity here to make clear that this can still return None.
        return None

    def SetHiddenDrawTag(self, node: c4d.BaseTag) -> None:
        """Creates the hidden drawing tag on a polygon object.
        """
        # The host is not a polygon object or there is already a valid drawing tag on that host.
        if (not isinstance(node, c4d.PolygonObject) or
                isinstance(self.GetHiddenDrawTag(node), c4d.VariableTag)):
            return

        # Flush the cache to remove any lingering tags and get the UUID of the polygon object so
        # that we cache the hidden draw tags.
        self.FlushHiddenTags(node)
        hostUuid: typing.Optional[memoryview] = node.FindUniqueID(c4d.MAXON_CREATOR_ID)
        if not isinstance(hostUuid, memoryview):
            return

        # Start creating the new tag, for that we must be on the main thread.
        if not c4d.threading.GeIsMainThread():
            return

        tag: c4d.VariableTag = c4d.VariableTag(c4d.Tvertexmap, node.GetPointCount())
        if not isinstance(tag, c4d.BaseTag):
            raise MemoryError("Could not allocate vertex map tag.")

        tag.AddUniqueID(VertexMapMixData.DRAWTAG_ID, bytes(True))
        tag.ChangeNBit(c4d.NBIT_OHIDE, True)
        node.InsertTag(tag)

        # Put the tag in the lookup table.
        self._hiddenDrawTags[bytes(hostUuid)] = tag

    def Init(self, node: c4d.GeListNode) -> bool:
        """Called by Cinema 4D to initialize the node.
        """
        self.InitAttr(node, c4d.BaseList2D, c4d.ID_SOURCE_DATA)
        self.InitAttr(node, bool, c4d.ID_DRAW_DATA)

        node[c4d.ID_SOURCE_DATA] = None
        node[c4d.ID_DRAW_DATA] = True

        # Attempt to create the tag, as all methods, Init is not guaranteed to be on the main thread.
        self.SetHiddenDrawTag(node.GetMain())
        return True

    def Free(self, node: c4d.GeListNode) -> None:
        """Called by Cinema 4D when the node is about to be destroyed.

        Will remove all hidden tags on the node.
        """
        self.FlushHiddenTags(node)

    def CopyTo(self, dest: c4d.plugins.NodeData, snode: c4d.GeListNode, dnode: c4d.GeListNode,
               flags: int, trn: c4d.AliasTrans) -> bool:
        """Called by Cinema 4D when a tag is being copied.

        Will remove all hidden tags on the source node, so that when a user drag-and_drops the tag 
        from object A to object B, object A is being cleaned up.
        """
        self.FlushHiddenTags(snode)
        return True

    def Message(self, node: c4d.GeListNode, type: int, data: object) -> bool:
        """Called by Cinema 4D to send messages to the node.

        We just use this here as something that is called often on the main thread. But there is
        no guarantee that this will run on the main thread, you were sending yourself a message, 
        MSG_UPDATE, to a node outside of the main thread in your TagData::Execute. Never assume
        Message to run on the main thread only.
        """
        # Try to create the tag, could be throttled by picking a specific message ID which is
        # called often enough.
        self.SetHiddenDrawTag(node.GetMain())
        return True

    def Execute(self, tag: c4d.BaseTag, doc: c4d.documents.BaseDocument, op: c4d.BaseObject,
                bt: c4d.threading.BaseThread, priority: int, flags: int) -> int:
        """Called by Cinema 4D to execute the tag.
        """
        state: bool = tag[c4d.ID_DRAW_DATA]
        src: typing.Optional[c4d.VariableTag] = tag[c4d.ID_SOURCE_DATA]
        dst: typing.Optional[c4d.VariableTag] = self.GetHiddenDrawTag(op)

        # There is no source or destination tag we could write from or to.
        if not isinstance(src, c4d.VariableTag) or not isinstance(dst, c4d.VariableTag):
            return c4d.EXECUTIONRESULT_OK

        # Select or deselect the hidden tag.
        if tag[c4d.ID_DRAW_DATA]:
            dst.SetBit(c4d.BIT_ACTIVE)
        else:
            dst.DelBit(c4d.BIT_ACTIVE)

        # Determine of we should copy the data and then copy the data.
        if (src.GetType() != dst.GetType() or
                src.GetDataCount() != dst.GetDataCount()):
            return c4d.EXECUTIONRESULT_OK
        dst.SetAllHighlevelData(src.GetAllHighlevelData())

        return c4d.EXECUTIONRESULT_OK


def RegisterPlugin() -> None:
    """Registers the plugin.
    """
    if not c4d.plugins.RegisterTagPlugin(
            id=VertexMapMixData.PLUGIN_ID,
            str=VertexMapMixData.PLUGIN_NAME,
            g=VertexMapMixData,
            description=VertexMapMixData.PLUGIN_DESCRIPTION,
            icon=c4d.bitmaps.InitResourceBitmap(c4d.FLgradient),
            info=c4d.TAG_EXPRESSION | c4d.TAG_VISIBLE | c4d.TAG_MULTIPLE):
        raise RuntimeError(f"Failed to register: {VertexMapMixData}")


if __name__ == "__main__":
    RegisterPlugin()

MAXON SDK Specialist
developers.maxon.net

Hello @rui_mac,

Thank you for reaching out to us. It depends on what you then considered to be a valid answer and what not. Generally speaking, the answer is no, not much has changed there. Vertex maps only draw their content when selected. Some bits and bytes for Python:

  • Vertex maps are only being drawn when selected, this however also applies to vertex maps in caches or hidden vertex maps. So, when you have some generator object and return a vertex map in its cache and set its bit BIT_ACTIVE, the generator will always be drawn as if the vertex map is selected. The user of course cannot see or interact with the tag because it is contained in the cache of the generator. The same applies for hidden (NBIT_OHIDE) tags.
  • You could apply this in the form that you have Python tag which constantly copies over data from a source tag to a hidden and selected tag. I have done this here just in the console:
    636e3f70-0324-4d76-811c-6a2714bac54b-image.png
  • Finally, you can just draw the data yourself. This is of course not the most performant thing in Python. Find a quick and dirty example as Python Scripting tag at the end of the posting.

Vertex maps are not something where you can change the behavior, e.g., make them draw themselves even when not selected. This also applies to C++, but you have a few more options there:

  • Manually drawing stuff into viewports is much faster there.
  • You can also implement there a CustomDataTagDisplayInterface. They are sort of a modern variant of variable tags and you have much more control over how and when they are being drawn (and they are also faster since you only must provide the vertex data and they then handle the drawing). A CustomDataTag is however not a VariableTag (and VertexMapTag by extension) and they can therefore not be used in place of vertex maps

Cheers,
Ferdinand

The file: vertexmap_example.c4d
The result:
a4df280a-b412-4e89-b6b2-9ad1ed6f8e3d-image.png
The code:

"""Draws a vertex map attached to the same object into the viewport.
"""
import c4d

doc: c4d.documents.BaseDocument # The document evaluating this tag
op: c4d.BaseTag # The Python scripting tag

def main() -> None:
    """Called by Cinema 4D to execute the Python tag, not used in this example.
    """
    pass

def draw(bd: c4d.BaseDraw) -> bool:
    """Called by Cinema 4D to let the tag draw into view ports.
    """
    # We could also limit this to specific view ports to ensure at least some performance ...
    cam: c4d.CameraObject = bd.GetSceneCamera(bd.GetDocument())
    name: str = bd.GetName()
    # Evaluate the camera of the vp or its name to bail on drawing ...

    # Bail when this is not the object draw pass
    if bd.GetDrawPass() != c4d.DRAWPASS_OBJECT:
        return

    # Get the object the Python tag is attached to.
    node: c4d.PolygonObject = op.GetMain()
    if not isinstance(node, c4d.PolygonObject):
        return

    # Find its first vertex map.
    vertexMapTag: c4d.VariableTag = node.GetTag(c4d.Tvertexmap)
    if vertexMapTag is None:
        return

    # Get the vertex map weights and the point and polygon data.
    data: list[float] = vertexMapTag.GetAllHighlevelData()
    points: list[c4d.Vector] = node.GetAllPoints()
    polygons: list[c4d.CPolygon] = node.GetAllPolygons()

    # Define a little lambda to convert [0, 1] weight values into yellow-red gradient as the
    # vertex map tag does.
    f: callable = lambda v: c4d.Vector(
        1,
        c4d.utils.RangeMap(v, 0, 1, 0, 1, True),
        0
    )

    # Setup the drawing space to the object space of #node and draw over polygons (offset 5).
    bd.SetMatrix_Matrix(node, node.GetMg(), zoffset=5)

    # Draw all polygons in #node manually, yikes, not fast :)
    for poly in polygons:
        bd.DrawPolygon(
            [points[poly.a], points[poly.b], points[poly.c], points[poly.d]],
            [f(data[poly.a]), f(data[poly.b]),f(data[poly.c]),f(data[poly.d])]
        )

    return True

MAXON SDK Specialist
developers.maxon.net

Thank you so much, @ferdinand .
The second option seems like the best one for me, if at all possible.
My plugin is already a tag.
So, is there any way to have the plugin selected, showing all the parameters in the Attributes Manager, and, at the same time, have a hidden VertexMap tag that shows the yellow-red gradient on the object where my plugin tag is active?

Just implemented it but the problem is that when I turn the hidden tag active, the Attribute Manager tells me that there are two objects selected (Multiple Objects) and the parameters of my plugin tag are no longer displayed.
Is there any way to keep both the hidden VertexMap tag and my plugin tag active, and display the parameters of my plugin tag in the Attribute Manager?

The only "solution", so far, is to manually activate the plugin tag, lock the Attribute Manager, and then activate the VertexMap tag.
Not an ideal "solution" :-(

Hello Remo,

Thank you for your updates,

@rui_mac said in Force VertexMap display:

Just implemented it but the problem is that when I turn the hidden tag active, the Attribute Manager tells me that there are two objects selected (Multiple Objects) and the parameters of my plugin tag are no longer displayed.
Is there any way to keep both the hidden VertexMap tag and my plugin tag active, and display the parameters of my plugin tag in the Attribute Manager?

I am not quite sure how you mean that, but I assume the underlying issue is here that I was not verbose enough in how at least I would implement that: You must constantly write from a source tag (or any other data source, does not have to be a vertex map) into a hidden vertex map and make sure that it stays selected.

I do not have any problems implementing this, including having the plugin interface being shown in the Attribute Manger, or just generally poking around in the tag selection state of the object. Find a quick example below.

If this was not what you meant, could you please restate what is going wrong for you? On a side note: We are exploiting here Cinema 4D behavior in an unintended manner, so this hack is not officially supported.

Cheers,
Ferdinand

The file:
tag_shenenigans.c4d
The result:
tag_shenengians.gif
The code:

"""Constantly copies over the data from a source vertex map to a hidden vertex map.

This can be used to mimic manually drawing a vertex map. You could even extend this to draw the 
union, intersection, difference, etc. of multiple tags. This quick and dirty solution will not flood
an object with dozens of tags by locking onto a unique ID on that hidden tag, but when the Python 
tag is removed, the hidden tag will linger. A more practical solution would be to implement this as a
TagData plugin where one could clean up after oneself when the tag is being moved or deleted.
"""
import c4d
import typing

doc: c4d.documents.BaseDocument # The document evaluating this tag
op: c4d.BaseTag # The Python scripting tag

# An ID to identify a hidden vertex map tag that has been crated for the purpose of this Python
# tag. We need this so that we can reestablish the linkage between this Python tag and a driver
# without flooding a scene/object with these hidden drawing tags. The better solution would be to
# bind the lifetime of such  drawing tag to the lifetime of a TagData plugin tag, but we cannot
# do this here.
ID_VM_DRAWTAG: int = 1060040

def GetDrawTag(node: c4d.PolygonObject) -> typing.Tuple[c4d.VariableTag, c4d.VariableTag]:
    """Retrieves or creates the hidden drawing tag of a polygon object.
    """
    # We loop over all tags on #node.
    tag: c4d.BaseTag = node.GetFirstTag()
    while isinstance(tag, c4d.BaseTag):
        # There usually will be at least two vertex map tags #node, the source and the hidden 
        # drawing tag. We need a safe way to identify such hidden drawing tag which is ours.
        # For that we use C4DAtom.AddUniqueID/FindUniqueID to mark such tags via ID_VM_DRAWTAG as
        # ours.
        if tag.CheckType(c4d.Tvertexmap):
            # Try to get the ID_VM_DRAWTAG UUID on a Tvertexmap tag.
            mem: typing.Optional[memoryview] = tag.FindUniqueID(ID_VM_DRAWTAG)
            # This overly cautious, but technically someone could have accidentally also written
            # to that ID, so we check if the stored data is indeed #True. When this is the case,
            # we have found the draw tag and return it.
            if isinstance(mem, memoryview):
                data: bytes = bytes(mem)
                try:
                    if bool(data) == True:
                        return tag
                except:
                    print ("Stepping over unexpected UUID data.")

        # Continue tag iteration on #node.
        tag = tag.GetNext()
    
    # When we have reached this line, it means there is no #ID_VM_DRAWTAG Tvertexmap tag on #node.
    # We create a new tag with the size of node, hide the tag and insert it under #node and then
    # return that new tag.
    tag: c4d.VariableTag = c4d.VariableTag(c4d.Tvertexmap, node.GetPointCount())
    if not isinstance(tag, c4d.BaseTag):
        raise MemoryError("Could not allocate vertex map tag.")
    
    tag.AddUniqueID(ID_VM_DRAWTAG, bytes(True))
    tag.ChangeNBit(c4d.NBIT_OHIDE, True)
    node.InsertTag(tag)

    return tag

def main() -> None:
    """Called by Cinema 4D to execute the Python tag.
    """
    # Get the data source vertex map from the user data. Again, this code is a bit overly cautious,
    # and expects the user data access to fail.
    try:
        sourceTag: c4d.VariableTag = op[c4d.ID_USERDATA, 1]
        if (not isinstance(sourceTag, c4d.VariableTag)) or (sourceTag.GetType() != c4d.Tvertexmap):
            return
    except:
        return

    # Get the polygon object the Python tag is attached to.
    node: c4d.PolygonObject = op.GetMain()
    if not isinstance(node, c4d.PolygonObject):
        return

    # Create or get the hidden "DrawTag" and bail when the source and draw tag do not match in size.
    drawTag: c4d.VariableTag = GetDrawTag(node)
    if sourceTag.GetDataCount() != drawTag.GetDataCount():
        return
    
    # Copy the data and make sure the hidden drawing tag is still selected.
    drawTag.SetAllHighlevelData(sourceTag.GetAllHighlevelData())
    drawTag.SetBit(c4d.BIT_ACTIVE)

MAXON SDK Specialist
developers.maxon.net

@ferdinand,

When I tried to implement this, as soon as I do the tag.ChangeNBit(c4d.NBIT_OHIDE, True) thing, the Attribute Manager changes to show the attributes of the VertexMap tag, and the parameters of my plugin tag, that create the hidden VertexMap tag disapear.
I'm at work now, but I will recheck my code agaisnt yours, later.
Thank you so much.

@rui_mac

You should not do this on an already inserted tag, the code I am showing above is as lined out very much in a grey area, or to be a bit more blunt, is a giant threading restriction violation. You should refrain from modifying the scene graph from outside of the main thread as much as you can, as this can introduce everything between odd behavior to outright crashes.

But I would say the singular insertion of the vertex map tag is somewhat safe. When you run into problems, you must defer all scene graph modifications, i.e., the insertion of that hidden tag, to the main thread. By for example using NodeData::Message to carry out something that before has been lined up by TagData::Execute.

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net

I was creating the hidden VertexMap in the Init method of my plugin tag.
But my main problem, like I described, was that activating the hidden VertexMap tag would show the VertexMap parameters in the Attribute Manager, instead of the parameters of my plugin tag.

Hey @rui_mac,

But my main problem, like I described, was that activating the hidden VertexMap tag would show the VertexMap parameters in the Attribute Manager

I assume you mean here that the tag node was made the "active object" of the Attribute Manager, i.e., it had the same effect as if you called c4d.gui.ActiveObjectManager_SetObject on the hidden tag? This could be a side-effect of 'misbehaving' in a threaded environment, I assume you are in TagData::Execute, but it seems unlikely. Could you please share an executable example of your code, because otherwise it will be very hard for me to help you.

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net

@ferdinand, here is a version of my code with all the stuff that is not important for this problem, removed. Although, I fully commented everything I removed.
I hope it is enough to spot the problem.

import c4d
import os
import sys
import math
import random

from c4d import gui,plugins,bitmaps,documents,utils,Matrix,BaseContainer
from c4d import threading

PLUGIN_ID = 1037312
CHECK_ID = 1037275
PLUGIN_NAME ="VertexMapMix"
PLUGIN_VERSION = "1.1"
url_access	= "vertexmapmix"

# define the IDs of the interface elements
VMM_ONOFF = 1000
VMM_VERTEXMAP = 1001
VMM_ATOMS = 1002
VMM_CREATEVERTEXTAG = 1003
VMM_SELECTVERTEXTAG = 1004

VMM_ATOM_ONOFF = 2001
VMM_ATOM_MIX = 2002
VMM_ATOM_INVERT = 2003
VMM_ATOM_SELECT = 2004
VMM_ATOM_NAME = 2005
VMM_ATOM_OPACITY = 2006
VMM_ATOM_BLUR = 2007

VMM_REMAPPING = 2020
VMM_ATOM_CURVE = 2021

VMM_SHOW = 2025

VMM_MIX_NORMAL = 0
VMM_MIX_MULT = 1
VMM_MIX_MIN = 2
VMM_MIX_MAX = 3
VMM_MIX_ADD = 4
VMM_MIX_SUB = 5

icon_edit = c4d.bitmaps.BaseBitmap()

# *******************************************************************************

class vertex_mm(plugins.TagData):

	old_selection	= 0
	old_pressed		= -1
	old_show 		= 1


	def Init(self,node):

		# in here I initialize all the GUI elements
		# ...

		# and in here I create the hidden VertexMap tag

		op = node.GetMain()
    	if not isinstance(op, c4d.PolygonObject): return True

			tags = op.GetTags()
			hidden_tag = None
			for tag in tags:
				if tag[c4d.ID_BASELIST_NAME] == "vmm_hidden":
					hidden_tag = tag
					break

			if hidden_tag == None:
				pnum=op.GetPointCount()
				hidden_tag=c4d.VariableTag(c4d.Tvertexmap,pnum)
				op.InsertTag(hidden_tag)
				hidden_tag[c4d.ID_BASELIST_NAME]="vmm_hidden"
				hidden_tag.ChangeNBit(c4d.NBIT_OHIDE,True)
		
		return True

# *******************************************************************************

	def Message(self,node,type,data):

		if node==None: return True

		# this code would turn the display of the VertexMap values on or off,
		# depending on the state of a checkbok

		if type==c4d.MSG_DESCRIPTION_VALIDATE:
			state =  node[VMM_SHOW]
			if state != self.old_show:
				self.old_show = state

				op = node.GetObject()
				if op.GetType() != 5100: return True

				tags = op.GetTags()
				hidden_tag = None
				for tag in tags:
					if tag[c4d.ID_BASELIST_NAME] == "vmm_hidden":
						hidden_tag = tag
						break

				if hidden_tag == None:
					pnum=op.GetPointCount()
					hidden_tag=c4d.VariableTag(c4d.Tvertexmap,pnum)
					op.InsertTag(hidden_tag)
					hidden_tag[c4d.ID_BASELIST_NAME]="vmm_hidden"

				hidden_tag.ChangeNBit(c4d.NBIT_OHIDE,True)
				if state == 1:
					hidden_tag.SetBit(c4d.BIT_ACTIVE)
					op.SetBit(c4d.BIT_ACTIVE)
					op.Message(c4d.MSG_UPDATE)
				else:
					hidden_tag.DelBit(c4d.BIT_ACTIVE)

		return True

# *******************************************************************************

	def Execute(self,tag,doc,op,bt,priority,flags):

		# here I do all the stuff that the plugin is supposed to do
		# at the end, in the list called w_array1, are all the values
		# to store in the hidden VertexMap tag, for display

		tags = op.GetTags()
		hidden_tag = None
		for tag in tags:
			if tag[c4d.ID_BASELIST_NAME] == "vmm_hidden":
				hidden_tag = tag
				break

		if hidden_tag != None:
			hidden_tag.SetAllHighlevelData(w_array1)

		vertex_mix.SetAllHighlevelData(w_array1)
		# the "vertex_mix" is a visible VertexMap tag, where the values are also stored.
		# IMPORTANT!!!
		# if it would be possible to make the VertexMap shading appear, simply by using
		# this visible VertexMap tag, I would be more than fine with this.
		# The problem, so far, is that I have to click this visible tag to make the
		# shading appear, and that makes all the parameters of my plugin Tag disapear
		# from the Attribute Manager.

		op.Message(c4d.MSG_UPDATE)

		return c4d.EXECUTIONRESULT_OK

# *******************************************************************************

# registration of the tag
if __name__=="__main__":

# Register the Tag
	icon = c4d.bitmaps.BaseBitmap()
	dir, file = os.path.split(__file__)
	icon.InitWith(os.path.join(dir, "res", "icon_vmm.tif"))
	icon_edit.InitWith(os.path.join(dir, "res", "edit.tif"))
	c4d.gui.RegisterIcon(1037349,icon_edit)

	plugins.RegisterTagPlugin(id=PLUGIN_ID, str=PLUGIN_NAME, g=vertex_mm,
						 description="vertexmapmix", icon=icon,
						 info=c4d.TAG_EXPRESSION|c4d.TAG_VISIBLE|c4d.TAG_MULTIPLE)
	print("["+PLUGIN_NAME+" Tag - v"+PLUGIN_VERSION+"] by Rui Batista: Loaded")

Hello @rui_mac,

I will have a look tomorrow; I was busy today.

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net

Hello @rui_mac,

So, I had a look at your code. It was unfortunately not executable in this form, even when I filled in some gaps like adding the necessary resources or re-indenting stuff. So, I cannot say many concrete things here, and only look at the general form.

I must also point out again, that this is a hack and not officially supported. If you chose to take this route instead of drawing the data yourself, possible side effects will be up to you to handle.

  • A general thing is that you seem to assume that Message, Init, and other methods to always run on the main thread. But there is no guarantee for that; when you want to carry out actions only allowed on the main thread, you must still check for being on the main thread. I however still think that adding a tag from a non-main thread was not your issue here.
  • This line in your Init is dangerous and might be the source for your problems, as you forcibly select the host object of your tag: op.SetBit(c4d.BIT_ACTIVE)
  • In general, you are also very generous with Message(c4d.MSG_UPDATE) calls. They are first of all here unnecessary for the flag changes and secondly also dangerous. I would never send messages to a node which is already part of the scene graph from threaded environment (you call op.Message(c4d.MSG_UPDATE) in multiple contexts).
  • Using the name of a node is a poor way of identifying a node, you should use UUIDs in Python.
  • I have provided below a TagData solution for your problem. This is quite a bit of busy work, especially when you want to properly clean up after yourself. This works fine for me.

if it would be possible to make the VertexMap shading appear, simply by using this visible VertexMap tag, I would be more than fine with this.

It would be the same, just iterate over the tags of the node hosting your tag and select whatever tags you want to select.

Cheers,
Ferdinand

The result:
9969441c-0ebc-4bca-a376-a693f5795e07-image.png
The plugin: pc14184.zip

The pyp file of the plugin:

"""Implements a tag which manages a hidden vertex map to draw data.
"""
import c4d
import typing

class VertexMapMixData(c4d.plugins.TagData):
    """Implements a tag which manages a hidden vertex map to draw data.
    """
    PLUGIN_ID: int = 1037312
    DRAWTAG_ID: int = 1060040

    PLUGIN_NAME: str = "VertexMapMix"
    PLUGIN_DESCRIPTION: str = "Tvertexmapmix"

    def __init__(self) -> None:
        """Initializes the instance, specifically its _hiddenDrawTags lookup table.
        """
        self._hiddenDrawTags: dict[bytes: c4d.BaseTag] = {}
        super().__init__()

    def FlushHiddenTags(self, node: c4d.GeListNode) -> None:
        """Flushes the #_hiddenDrawTags cache and removes all the hidden tags from the hosting node.
        """
        host: typing.Optional[c4d.BaseObject] = node.GetMain()
        if not isinstance(host, c4d.BaseObject):
            return

        tag: c4d.BaseTag
        for tag in host.GetTags():
            if tag.GetType() != c4d.Tvertexmap:
                continue

            # This is a hidden drawing tag instance.
            uuid: typing.Optional[memoryview] = tag.FindUniqueID(VertexMapMixData.DRAWTAG_ID)
            if isinstance(uuid, memoryview):
                tag.Remove()

        self._hiddenDrawTags = {}

    def GetHiddenDrawTag(self, node: c4d.PolygonObject) -> typing.Optional[c4d.VariableTag]:
        """Retrieves the hidden drawing tag on a polygon object.
        """
        # Bail when #node is not a polygon object or not attached to a document.
        if (not isinstance(node, c4d.PolygonObject) or
                not isinstance(node.GetDocument(), c4d.documents.BaseDocument)):
            return None

        # Get the UUID of the polygon object so that we look up its hidden tag in the lookup.
        uuid: typing.Optional[memoryview] = node.FindUniqueID(c4d.MAXON_CREATOR_ID)
        if not isinstance(uuid, memoryview):
            return None

        # Check if there is already a cached hidden draw tag in dictionary of the plugin interface.
        hiddenTag: typing.Optional[c4d.BaseTag] = self._hiddenDrawTags.get(bytes(uuid), None)
        if isinstance(hiddenTag, c4d.BaseTag) and hiddenTag.IsAlive():
            return hiddenTag

        # Just for verbosity here to make clear that this can still return None.
        return None

    def SetHiddenDrawTag(self, node: c4d.BaseTag) -> None:
        """Creates the hidden drawing tag on a polygon object.
        """
        # The host is not a polygon object or there is already a valid drawing tag on that host.
        if (not isinstance(node, c4d.PolygonObject) or
                isinstance(self.GetHiddenDrawTag(node), c4d.VariableTag)):
            return

        # Flush the cache to remove any lingering tags and get the UUID of the polygon object so
        # that we cache the hidden draw tags.
        self.FlushHiddenTags(node)
        hostUuid: typing.Optional[memoryview] = node.FindUniqueID(c4d.MAXON_CREATOR_ID)
        if not isinstance(hostUuid, memoryview):
            return

        # Start creating the new tag, for that we must be on the main thread.
        if not c4d.threading.GeIsMainThread():
            return

        tag: c4d.VariableTag = c4d.VariableTag(c4d.Tvertexmap, node.GetPointCount())
        if not isinstance(tag, c4d.BaseTag):
            raise MemoryError("Could not allocate vertex map tag.")

        tag.AddUniqueID(VertexMapMixData.DRAWTAG_ID, bytes(True))
        tag.ChangeNBit(c4d.NBIT_OHIDE, True)
        node.InsertTag(tag)

        # Put the tag in the lookup table.
        self._hiddenDrawTags[bytes(hostUuid)] = tag

    def Init(self, node: c4d.GeListNode) -> bool:
        """Called by Cinema 4D to initialize the node.
        """
        self.InitAttr(node, c4d.BaseList2D, c4d.ID_SOURCE_DATA)
        self.InitAttr(node, bool, c4d.ID_DRAW_DATA)

        node[c4d.ID_SOURCE_DATA] = None
        node[c4d.ID_DRAW_DATA] = True

        # Attempt to create the tag, as all methods, Init is not guaranteed to be on the main thread.
        self.SetHiddenDrawTag(node.GetMain())
        return True

    def Free(self, node: c4d.GeListNode) -> None:
        """Called by Cinema 4D when the node is about to be destroyed.

        Will remove all hidden tags on the node.
        """
        self.FlushHiddenTags(node)

    def CopyTo(self, dest: c4d.plugins.NodeData, snode: c4d.GeListNode, dnode: c4d.GeListNode,
               flags: int, trn: c4d.AliasTrans) -> bool:
        """Called by Cinema 4D when a tag is being copied.

        Will remove all hidden tags on the source node, so that when a user drag-and_drops the tag 
        from object A to object B, object A is being cleaned up.
        """
        self.FlushHiddenTags(snode)
        return True

    def Message(self, node: c4d.GeListNode, type: int, data: object) -> bool:
        """Called by Cinema 4D to send messages to the node.

        We just use this here as something that is called often on the main thread. But there is
        no guarantee that this will run on the main thread, you were sending yourself a message, 
        MSG_UPDATE, to a node outside of the main thread in your TagData::Execute. Never assume
        Message to run on the main thread only.
        """
        # Try to create the tag, could be throttled by picking a specific message ID which is
        # called often enough.
        self.SetHiddenDrawTag(node.GetMain())
        return True

    def Execute(self, tag: c4d.BaseTag, doc: c4d.documents.BaseDocument, op: c4d.BaseObject,
                bt: c4d.threading.BaseThread, priority: int, flags: int) -> int:
        """Called by Cinema 4D to execute the tag.
        """
        state: bool = tag[c4d.ID_DRAW_DATA]
        src: typing.Optional[c4d.VariableTag] = tag[c4d.ID_SOURCE_DATA]
        dst: typing.Optional[c4d.VariableTag] = self.GetHiddenDrawTag(op)

        # There is no source or destination tag we could write from or to.
        if not isinstance(src, c4d.VariableTag) or not isinstance(dst, c4d.VariableTag):
            return c4d.EXECUTIONRESULT_OK

        # Select or deselect the hidden tag.
        if tag[c4d.ID_DRAW_DATA]:
            dst.SetBit(c4d.BIT_ACTIVE)
        else:
            dst.DelBit(c4d.BIT_ACTIVE)

        # Determine of we should copy the data and then copy the data.
        if (src.GetType() != dst.GetType() or
                src.GetDataCount() != dst.GetDataCount()):
            return c4d.EXECUTIONRESULT_OK
        dst.SetAllHighlevelData(src.GetAllHighlevelData())

        return c4d.EXECUTIONRESULT_OK


def RegisterPlugin() -> None:
    """Registers the plugin.
    """
    if not c4d.plugins.RegisterTagPlugin(
            id=VertexMapMixData.PLUGIN_ID,
            str=VertexMapMixData.PLUGIN_NAME,
            g=VertexMapMixData,
            description=VertexMapMixData.PLUGIN_DESCRIPTION,
            icon=c4d.bitmaps.InitResourceBitmap(c4d.FLgradient),
            info=c4d.TAG_EXPRESSION | c4d.TAG_VISIBLE | c4d.TAG_MULTIPLE):
        raise RuntimeError(f"Failed to register: {VertexMapMixData}")


if __name__ == "__main__":
    RegisterPlugin()

MAXON SDK Specialist
developers.maxon.net

@ferdinand, thank you for the excellent advices.
I hope I can get home soon enough after work to clean up my code according to your advices and implement your solution.
Also, your advices will allow me to clean up some of my other plugins.
Thank you very much, once again.

Well, it kind of works. When I adjust parameters in the plugin tag, sometimes the Attribute Manager reverts to show "2 Elements [VertexMap,ThePluginName]"
I even tried with your plugin and, by twirling the Data Source parameter open and, for example, turning the Invert option on, the Attribute Manager stops showing the plugin parameters, and starts showing "2 Elements [VertexMap,VertexMapMax]"

Hello @rui_mac,

ah, okay, I understand what you mean. Yeah, that is a bit odd and also makes this solution not very viable. As I said before, this always was a hack. When I did this myself a few years ago, I think I never ran into this issue.

The underlying problem here is that the Attribute Manager is able to display a hidden tag as selected and the whole setup relies on the fact that hidden tags are being ignored in that context. It is also very erratic how this happens, when I for example click long enough on the Draw Data bool, the problem will also occur at some point and at the same time, it sometimes it does not happen 'inside' the vertexmap when I change a parameter there.

Since this seems to be tied to parameter writing events, you could try to switch off the hidden tag being selected before a parameter is being written and turn it back on after that. But that will become really complicated or even possible to do for 'buried' parameters changes, such as for example, changing something in a field driving a vertex map which is linked in the tag.

I will file this as a bug because the Attribute Manager should not be able to display hidden tags in single or multi-selections. In the meantime, I can only recommend using C++ and drawing things yourself or using CustomDataTagDisplayInterface, as this will be performant and is also guaranteed to work.

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net

@ferdinand, thank you for the answer and explanation. I will try to find a compromise.
In the meanwhile, one can always select the tag, lock the Attribute Manager and click the VertexMap tag to display the result. Cumbersome, but it works.