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:

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