Hi,
thanks for the kind words both from Maxon and the community. I am looking forward to my upcoming adventures with the SDK Team and Cinema community.
Cheers,
Ferdinand
Hi,
thanks for the kind words both from Maxon and the community. I am looking forward to my upcoming adventures with the SDK Team and Cinema community.
Cheers,
Ferdinand
Hello @holgerbiebrach,
please excuse the wait. So, this is possible in Python and quite easy to do. This new behavior is just the old dialog folding which has been reworked a little bit. I have provided a simple example at the end of the posting. There is one problem regarding title bars which is sort of an obstacle for plugin developers which want to distribute their plugins, it is explained in the example below.
I hope this helps and cheers,
Ferdinand
The result:
The code:
"""Example for a command plugin with a foldable dialog as provided with the
Asset Browser or Coordinate Manger in Cinema 4D R25.
The core of this is just the old GeDialog folding mechanic which has been
changed slightly with R25 as it will now also hide the title bar of a folded
dialog, i.e., the dialog will be hidden completely.
The structure shown here mimics relatively closely what the Coordinate Manger
does. There is however one caveat: Even our internal implementations do not
hide the title bar of a dialog when unfolded. Instead, this is done via
layouts, i.e., by clicking onto the ≡ icon of the dialog and unchecking the
"Show Window Title" option and then saving such layout. If you would want
to provide a plugin which exactly mimics one of the folding managers, you
would have to either ask your users to take these steps or provide a layout.
Which is not ideal, but I currently do not see a sane way to hide the title
bar of a dialog. What you could do, is open the dialog as an async popup which
would hide the title bar. But that would also remove the ability to dock the
dialog. You could then invoke `GeDialog.AddGadegt(c4d.DIALOG_PIN, SOME_ID)`to
manually add a pin back to your dialog, so that you can dock it. But that is
not how it is done internally by us, as we simply rely on layouts for that.
"""
import c4d
class ExampleDialog (c4d.gui.GeDialog):
"""Example dialog that does nothing.
The dialog itself has nothing to do with the implementation of the
folding.
"""
ID_GADGETS_START = 1000
ID_GADGET_GROUP = 0
ID_GADGET_LABEL = 1
ID_GADGET_TEXT = 2
GADGET_STRIDE = 10
GADEGT_COUNT = 5
def CreateLayout(self) -> bool:
"""Creates dummy gadgets.
"""
self.SetTitle("ExampleDialog")
flags = c4d.BFH_SCALEFIT
for i in range(self.GADEGT_COUNT):
gid = self.ID_GADGETS_START + i * self.GADGET_STRIDE
name = f"Item {i}"
self.GroupBegin(gid + self.ID_GADGET_GROUP, flags, cols=2)
self.GroupBorderSpace(5, 5, 5, 5)
self.GroupSpace(2, 2)
self.AddStaticText(gid + self.ID_GADGET_LABEL, flags, name=name)
self.AddEditText(gid + self.ID_GADGET_TEXT, flags)
self.GroupEnd()
return True
class FoldingManagerCommand (c4d.plugins.CommandData):
"""Provides the implementation for a command with a foldable dialog.
"""
ID_PLUGIN = 1058525
REF_DIALOG = None
@property
def Dialog(self) -> ExampleDialog:
"""Returns a class bound ExampleDialog instance.
"""
if FoldingManagerCommand.REF_DIALOG is None:
FoldingManagerCommand.REF_DIALOG = ExampleDialog()
return FoldingManagerCommand.REF_DIALOG
def Execute(self, doc: c4d.documents.BaseDocument) -> bool:
"""Folds or unfolds the dialog.
The core of the folding logic as employed by the Asset Browser
or the Coordinate manager in R25.
"""
# Get the class bound dialog reference.
dlg = self.Dialog
# Fold the dialog, i.e., hide it if it is open and unfolded. In C++
# you would also want to test for the dialog being visible with
# GeDialog::IsVisible, but we cannot do that in Python.
if dlg.IsOpen() and not dlg.GetFolding():
dlg.SetFolding(True)
# Open or unfold the dialog. The trick here is that calling
# GeDialog::Open will also unfold the dialog.
else:
dlg.Open(c4d.DLG_TYPE_ASYNC, FoldingManagerCommand.ID_PLUGIN)
return True
def RestoreLayout(self, secret: any) -> bool:
"""Restores the dialog on layout changes.
"""
return self.Dialog.Restore(FoldingManagerCommand.ID_PLUGIN, secret)
def GetState(self, doc: c4d.documents.BaseDocument) -> int:
"""Sets the command icon state of the plugin.
This is not required, but makes it a bit nicer, as it will indicate
in the command icon when the dialog is folded and when not.
"""
dlg = self.Dialog
result = c4d.CMD_ENABLED
if dlg.IsOpen() and not dlg.GetFolding():
result |= c4d.CMD_VALUE
return result
def RegisterFoldingManagerCommand() -> bool:
"""Registers the example.
"""
return c4d.plugins.RegisterCommandPlugin(
id=FoldingManagerCommand.ID_PLUGIN,
str="FoldingManagerCommand",
info=c4d.PLUGINFLAG_SMALLNODE,
icon=None,
help="FoldingManagerCommand",
dat=FoldingManagerCommand())
if __name__ == '__main__':
if not RegisterFoldingManagerCommand():
raise RuntimeError(
f"Failed to register {FoldingManagerCommand} plugin.")
Dear Community,
this question reached us via email-support in the context of C++, but I thought the answer might be interesting for other users too.
The underlying question in this case was how to project points from object or world space into the texture space of an object with UV data. I am showing here deliberately an approach that can be followed both in C++ and Python, so that all users can benefit from this. In C++ one has also the option of using VolumeData and its methods VolumeData::GetUvw
or VolumeData::ProjectPoint
but must then either implement a volume shader (as otherwise the volume data attached to the ChannelData
passed to ShaderData::Output
will be nullptr
), or use VolumeData:: AttachVolumeDataFake
to access ::ProjectPoint
. There is however no inherent necessity to take this shader bound route as shown by the example.
Cheers,
Ferdinand
The script has created a texture with red pixels for the intersection points of the rays cast from each vertex of the spline towards the origin of the polygon object. The script also created the null object rays to visualize the rays which have been cast.
raycast_texture.c4d : The scene file.
You must save the script to disk before running it, as the script infers from the script location the place to save the generated texture to.
"""Demonstrates how to project points from world or object space to UV space.
This script assumes that the user has selected a polygon object and a spline object in the order
mentioned. The script projects the points of the spline object onto the polygon object and creates
a texture from the UV coordinates of the projected points. The texture is then applied to the
polygon object.
The script uses the `GeRayCollider` class to find the intersection of rays cast from the points of
the spline object to the polygon object. The UV coordinates of the intersection points are then
calculated using the `HairLibrary` class. In the C++ API, one should use maxon::
GeometryUtilsInterface::CalculatePolygonPointST() instead.
Finally, using GeRayCollider is only an example for projecting points onto the mesh. In practice,
any other method can be used as long as it provides points that lie in the plane(s) of a polygon.
The meat of the example is in the `main()` function. The other functions are just fluff.
"""
import os
import c4d
import mxutils
import uuid
from mxutils import CheckType
doc: c4d.documents.BaseDocument # The currently active document.
op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`.
def CreateTexture(points: list[c4d.Vector], path: str, resolution: int = 1000) -> None:
"""Creates a texture from the given `points` and saves it to the given `path`.
Parameters:
path (str): The path to save the texture to.
points (list[c4d.Vector]): The points to create the texture from.
"""
# Check the input values for validity.
if os.path.exists(path):
raise FileExistsError(f"File already exists at path: {path}")
if not path.endswith(".png"):
raise ValueError("The path must end with '.png'.")
# Create a drawing canvas to draw the points on.
canvas: c4d.bitmaps.GeClipMap = CheckType(c4d.bitmaps.GeClipMap())
if not canvas.Init(resolution, resolution, 24):
raise MemoryError("Failed to initialize GeClipMap.")
# Fill the canvas with white.
canvas.BeginDraw()
canvas.SetColor(255, 255, 255)
canvas.FillRect(0, 0, resolution, resolution)
# Draw the points on the canvas.
canvas.SetColor(255, 0, 0)
for p in points:
x: int = int(p.x * resolution)
y: int = int(p.y * resolution)
x0: int = max(0, x - 1)
y0: int = max(0, y - 1)
x1: int = min(resolution, x + 1)
y1: int = min(resolution, y + 1)
canvas.FillRect(x0, y0, x1, y1)
canvas.EndDraw()
# Save the canvas to the given path.
bitmap: c4d.bitmaps.BaseBitmap = CheckType(canvas.GetBitmap())
bitmap.Save(path, c4d.FILTER_PNG)
c4d.bitmaps.ShowBitmap(bitmap)
def ApplyTexture(obj: c4d.BaseObject, path: str) -> None:
"""Applies the texture at the given `path` to the given `obj`.
"""
CheckType(obj, c4d.BaseObject)
# Check the input values for validity.
if not os.path.exists(path):
raise FileNotFoundError(f"File does not exist at path: {path}")
# Create a material and apply the texture to it.
material: c4d.BaseMaterial = CheckType(c4d.BaseMaterial(c4d.Mmaterial), c4d.BaseMaterial)
obj.GetDocument().InsertMaterial(material)
shader: c4d.BaseShader = CheckType(c4d.BaseShader(c4d.Xbitmap), c4d.BaseShader)
shader[c4d.BITMAPSHADER_FILENAME] = path
material.InsertShader(shader)
material[c4d.MATERIAL_COLOR_SHADER] = shader
material[c4d.MATERIAL_PREVIEWSIZE] = c4d.MATERIAL_PREVIEWSIZE_1024
# Apply the material to the object.
tag: c4d.TextureTag = CheckType(obj.MakeTag(c4d.Ttexture))
tag[c4d.TEXTURETAG_PROJECTION] = c4d.TEXTURETAG_PROJECTION_UVW
tag[c4d.TEXTURETAG_MATERIAL] = material
def CreateDebugRays(spline: c4d.SplineObject, p: c4d.Vector) -> None:
"""Adds spline objects to the document to visualize the rays from the given `p` to the points of
the given `spline`.
"""
doc: c4d.documents.BaseDocument = CheckType(spline.GetDocument(), c4d.documents.BaseDocument)
rays: c4d.BaseObject = c4d.BaseObject(c4d.Onull)
rays.SetName("Rays")
doc.InsertObject(rays)
for q in spline.GetAllPoints():
ray: c4d.SplineObject = c4d.SplineObject(2, c4d.SPLINETYPE_LINEAR)
ray.SetPoint(0, p)
ray.SetPoint(1, q * spline.GetMg())
ray.Message(c4d.MSG_UPDATE)
ray.InsertUnder(rays)
def main() -> None:
"""Carries out the main logic of the script.
"""
# Check the object selection for being meaningful input.
selected: list[c4d.BaseObject] = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER)
if (len(selected) != 2 or not selected[0].CheckType(c4d.Opolygon) or
not selected[1].CheckType(c4d.Ospline)):
raise ValueError("Please select a polygon object and a spline object.")
polygonObject, splineObject = selected
# Get the uvw tag, the points, and the polygons of the polygon object.
uvwTag: c4d.UvwTag = mxutils.CheckType(polygonObject.GetTag(c4d.Tuvw))
points: list[c4d.Vector] = [polygonObject.GetMg() * p for p in polygonObject.GetAllPoints()]
polys: list[c4d.CPolygon] = polygonObject.GetAllPolygons()
# We are casting here in a dumb manner towards the center of the polygon object. In practice,
# one should cast rays towards the plane of the polygon object. Or even better, use another
# method to project the points onto the polygon object, as GeRayCollider is not the most
# efficient thing in the world.
rayTarget: c4d.Vector = polygonObject.GetMg().off
CreateDebugRays(splineObject, rayTarget)
# Initialize the GeRayCollider to find the intersection of rays cast from the points of the
# spline object to the polygon object.
collider: c4d.utils.GeRayCollider = c4d.utils.GeRayCollider()
if not collider.Init(polygonObject):
raise MemoryError("Failed to initialize GeRayCollider.")
# Init our output list and iterate over the points of the spline object.
uvPoints: list[c4d.Vector] = []
for p in splineObject.GetAllPoints():
# Transform the point from object to world space (q) and then to the polygon object's space
# (ro). Our ray direction always points towards the center of the polygon object.
q: c4d.Vector = splineObject.GetMg() * p
ro: c4d.Vector = ~polygonObject.GetMg() * q
rd: c4d.Vector = rayTarget - ro
# Cast the ray and check if it intersects with the polygon object.
if not collider.Intersect(ro, rd, 1E6) or collider.GetIntersectionCount() < 1:
continue
# Get the hit position and the polygon ID of the intersection.
hit: dict = collider.GetNearestIntersection()
pos: c4d.Vector = mxutils.CheckType(hit.get("hitpos", None), c4d.Vector)
pid: int = mxutils.CheckType(hit.get("face_id", None), int)
# One mistake would be now to use the barycentric coordinates that are in the intersection
# data, as Cinema uses an optimized algorithm to interpolate in a quad and not the standard
# cartesian-barycentric conversion. In Python these polygon weights are only exposed in a
# bit weird place, the hair library. In C++ these barycentric coordinates make sense because
# there exist methods to convert them to weights. In Python the barycentric coordinates are
# pretty much useless as we do not have such a conversion function here.
# Compute the weights s, t for the intersection point in the polygon.
s, t = c4d.modules.hair.HairLibrary().GetPolyPointST(
pos, points[polys[pid].a], points[polys[pid].b],
points[polys[pid].c], points[polys[pid].d], True)
# Get the uv polygon and bilinearly interpolate the coordinates using the weights. It would
# be better to use the more low-level variable tag data access functions in VariableTag
# than UvwTag.GetSlow() in a real-world scenario.
uvw: list[c4d.Vector] = list(uvwTag.GetSlow(pid).values())
t0: c4d.Vector = c4d.utils.MixVec(uvw[0], uvw[1], s)
t1: c4d.Vector = c4d.utils.MixVec(uvw[3], uvw[2], s)
uv: c4d.Vector = c4d.utils.MixVec(t0, t1, t)
# Append the UV coordinates to the output list.
uvPoints.append(uv)
# Write the UV coordinates to a texture and apply it to the polygon object.
path: str = os.path.join(os.path.dirname(__file__), f"image-{uuid.uuid4()}.png")
CreateTexture(uvPoints, path, resolution=1024)
ApplyTexture(polygonObject, path)
c4d.EventAdd()
if __name__ == '__main__':
main()
Dear community,
We recently got a non-public support request about handling drag and drop events for a custom image control in a dialog. And while we have shown this, drag event handling, multiple times before, I thought it does not hurt to answer this publicly, as this very case - an image control - is quite common, and it does not hurt having a bit more verbose public example for it.
edit: I have updated this topic with new code and a new video, to also handle outgoing asset drag events, and talk a bit about the pros and cons of different approaches, and what you could do when not implementing a code example as I did here, and therefore have a bit more leverage room when it comes to complexity.
In general, doing all this is possible. It is just that not everything is a ready-made solution for you, where you just call a function. At some point you have to "swim" here yourself, as we cannot write your code for you, but I hope the example helps shedding some light on the probably uncessarily complicated drag handling in Cinema 4D.
Cheers,
Ferdinand
"""Provides an example for implementing a custom control that can generate and receive drag events.
The example implements a dialog which holds multiple "cards" (BitmapCard) that display an image
and can receive drag events. The user can drag images from the file system into these cards (file
paths) or drag texture assets from the Asset Browser into these cards. The cards can also generate
drag events themselves, e.g., when the user drags from a card, it will generate a drag event.
Showcased (selectable via the combo box in the menu of the dialog) are three drag types:
1. File paths: Dragging a card will generate an image file path drag event, which for example could
be dragged into a file path field of a shader, onto an object in the viewport, or onto other
BitmapCard controls. The advantage of this approach is that we do not have to create
materials or assets, but can just use the file path directly.
2. Materials: Dragging a card will generate an atom drag event, here populated with a Redshift
material that has the texture of the card as an input. This material can then be dragged onto
anything that accepts materials, like objects in the viewport. The disadvantage of this
approach is that we have to create a material, and that we have to exactly define how the
material is constructed.
3. Assets: Dragging a card will generate an asset drag event, here populated with a texture asset
that has the texture of the card as an input. This asset can then be dragged onto anything that
texture accepts assets, such as an object in the viewport. The disadvantage of this
approach is that we have to create an asset.
- The example also showcases a variation oof this approach, where we exploit a bit how the
texture asset drag handling works, to avoid having to create an asset.
Overview:
- BitmapCard: A user area control that can receive drag events and generate drag events itself. Here
you will find almost all the relevant code for drag and drop handling
- BitmapStackDialog: A dialog that holds multiple BitmapCard controls and allows the user to select
the drag type via a combo box. This dialog is the main entry point of the example
but does not contain much relevant code itself.
"""
__author__ = "Ferdinand Hoppe"
__copyright__ = "Copyright 2025 Maxon Computer GmbH"
import os
import re
import c4d
import maxon
import mxutils
# The file types that are supported to be dragged into a BitmapCard control.
DRAG_IN_FILE_TYPES: list[str] = [".png", ".jpg", ".jpeg", ".tif"]
# A regex to check of a string (which is meant to be a path/url) already as a scheme (like file:///
# or http:///). The protocol we are mostly checking for is "asset:///", but we keep this regex generic
# to allow for other schemes as well.
RE_URL_HAS_SCHEME = re.compile("^[a-zA-Z][a-zA-Z\d+\-.]*:\/\/\/")
# A non-public core message which need when we want to properly generate asset drag events.
COREMSG_SETCOMMANDEDITMODE: int = 300001000
class BitmapCard(c4d.gui.GeUserArea):
"""Implements a control that can receive bitmap path drag events and generates drag events itself.
"""
def __init__(self, host: "BitmapStackDialog") -> None:
"""Constructs a new BitmapCard object.
"""
# The host dialog that holds this card. This is used to access the drag type selected in the
# combo box of the dialog.
self._host: BitmapStackDialog = host
# The image file path and the bitmap that is displayed in this card.
self._path: str | None = ""
self._bitmap: c4d.bitmaps.BaseBitmap | None = None
def GetMinSize(self) -> tuple[int, int]:
"""Called by Cinema 4d to evaluate the minimum size of the user area.
"""
return 250, 100
def DrawMsg(self, x1, y1, x2, y2, msg_ref):
"""Called by Cinema 4D to let the user area draw itself.
"""
self.OffScreenOn()
self.SetClippingRegion(x1, y1, x2, y2)
# Draw the background and then the bitmap if it exists. In the real world, we would have to
# either adapt #GetMinSize to the size of the bitmap, or draw the bitmap in a way that it
# fits into the user area, e.g., by scaling it down. Here we just draw it at a fixed size.
self.DrawSetPen(c4d.gui.GetGuiWorldColor(c4d.COLOR_BGGADGET))
self.DrawRectangle(x1, y1, x2, y2)
if self._bitmap:
w, h = self._bitmap.GetSize()
self.DrawBitmap(self._bitmap, 5, 5, 240, 90, 0,
0, w, h, c4d.BMP_NORMALSCALED)
def Message(self, msg: c4d.BaseContainer, result: c4d.BaseContainer) -> int:
"""Called by Cinema 4D to handle messages sent to the user area.
Here we implement receiving drag events when the user drags something onto this user area.
"""
# A drag event is coming in.
if msg.GetId() == c4d.BFM_DRAGRECEIVE:
# Get out when the drag event has been discarded.
if msg.GetInt32(c4d.BFM_DRAG_LOST) or msg.GetInt32(c4d.BFM_DRAG_ESC):
return self.SetDragDestination(c4d.MOUSE_FORBIDDEN)
# Get out when this is not a drag type we support (we just support images). Note that we
# cannot make up drag types ourselves, so when we want to send/receive complex data, we
# must use the DRAGTYPE_ATOMARRAY type and pack our data into a BaseContainer attached
# to the nodes we drag/send.
data: dict = self.GetDragObject(msg)
dragType: int = data.get("type", 0)
if dragType not in [c4d.DRAGTYPE_FILENAME_IMAGE, maxon.DRAGTYPE_ASSET]:
return self.SetDragDestination(c4d.MOUSE_FORBIDDEN)
# Here we could optionally check the drag event hitting some target area.
# yPos: float = self.GetDragPosition(msg).get('y', 0.0)
# if not 0 < yPos < 50 or not self.CheckDropArea(msg, True, True):
# return self.SetDragDestination(c4d.MOUSE_FORBIDDEN)
# After this point, we are dealing with a valid drag event.
# The drag is still going on, we just set the mouse cursor to indicate that we are
# ready to receive the drag event.
if msg.GetInt32(c4d.BFM_DRAG_FINISHED) == 0:
return self.SetDragDestination(c4d.MOUSE_MOVE)
# The drag event is finished, we can now process the data.
else:
# Unpack a file being dragged directly.
path: str | None = None
if dragType == c4d.DRAGTYPE_FILENAME_IMAGE:
path = data.get("object", None)
# Unpack an asset being dragged.
elif dragType == maxon.DRAGTYPE_ASSET:
array: maxon.DragAndDropDataAssetArray = data.get(
"object", None)
descriptions: tuple[tuple[maxon.AssetDescription, maxon.Url, maxon.String]] = (
array.GetAssetDescriptions())
if not descriptions:
# Invalid asset but we are not in an error state.
return True
# Check that we are dealing with an image asset.
asset: maxon.AssetDescription = descriptions[0][0]
metadata: maxon.BaseContainer = asset.GetMetaData()
subType: maxon.Id = metadata.Get(
maxon.ASSETMETADATA.SubType, maxon.Id())
if (subType != maxon.ASSETMETADATA.SubType_ENUM_MediaImage):
# Invalid asset but we are not in an error state.
return True
path = str(maxon.AssetInterface.GetAssetUrl(asset, True))
if not isinstance(path, str):
return False # Critical failure.
if (dragType == c4d.DRAGTYPE_FILENAME_IMAGE and
(not os.path.exists(path) or
os.path.splitext(path)[1].lower() not in DRAG_IN_FILE_TYPES)):
# Invalid file type but we are not in an error state.
return True
self._path = path
self._bitmap = c4d.bitmaps.BaseBitmap()
if self._bitmap.InitWith(path)[0] != c4d.IMAGERESULT_OK:
return False # Critical failure.
self.Redraw() # Redraw the user area to show the new bitmap.
return True
# Process other messages, doing this is very important, as we otherwise break the
# message chain.
return c4d.gui.GeUserArea.Message(self, msg, result)
def InputEvent(self, msg: c4d.BaseContainer) -> bool:
"""Called by Cinema 4D when the user area receives input events.
Here we implement creating drag events when the user drags from this user area. The type of
drag event which is initiated is determined by the drag type selected in the combo box
of the dialog.
"""
# When this is not a left mouse button event on this user area, we just get out without
# consuming the event (by returning False).
if (msg.GetInt32(c4d.BFM_INPUT_DEVICE) != c4d.BFM_INPUT_MOUSE or
msg.GetInt32(c4d.BFM_INPUT_CHANNEL) != c4d.BFM_INPUT_MOUSELEFT):
return False
# Get the type of drag event that should be generated, and handle it.
dragType: int = self._host.GetInt32(self._host.ID_DRAG_TYPE)
return self.HandleDragEvent(msg, dragType)
# --- Custom drag handling methods -------------------------------------------------------------
# I have split up thing into three methods for readability, this all could also be done directly
# in the InputEvent method.
def HandleDragEvent(self, event: c4d.BaseContainer, dragType: int) -> bool:
"""Handles starting a drag event by generating the drag data and sending it to the system.
This is called when the user starts dragging from this user area.
"""
# This requires us to modify the document, so we must be on the main thread (technically
# not true when the type is DRAGTYPE_FILENAME_IMAGE, but that would be for you to optimize).
if not c4d.threading.GeIsMainThread():
raise False
# Generate our drag data, either a file path, a material, or an asset.
doc: c4d.documents.BaseDocument = c4d.documents.GetActiveDocument()
data: object = self.GenerateDragData(doc, dragType)
# Now we set off the drag event. When we are dragging assets, we have to sandwich the event
# in these core message calls, as otherwise the palette edit mode toggling will not work
# correctly.
if dragType == maxon.DRAGTYPE_ASSET:
bc: c4d.BaseContainer = c4d.BaseContainer(
COREMSG_SETCOMMANDEDITMODE)
bc.SetInt32(1, True)
c4d.SendCoreMessage(c4d.COREMSG_CINEMA, bc, 0)
# When #HandleMouseDrag returns #False, this means that the user has cancelled the drag
# event, one case could be that the user actually did not intend to drag, but just
# clicked on the user area.
#
# In this case we remove the drag data we generated, as it is not needed anymore. Note that
# this will NOT catch the case that the drag event is cancelled by the recipient, e.g., the
# user drags a material onto something that does not accept materials.
if not self.HandleMouseDrag(event, dragType, data, 0):
self.RemoveDragData(doc, data)
return True
# Other half of the sandwich, we toggle the palette edit mode back to normal.
if dragType == maxon.DRAGTYPE_ASSET:
bc: c4d.BaseContainer = c4d.BaseContainer(
COREMSG_SETCOMMANDEDITMODE)
bc.SetInt32(1, False)
c4d.SendCoreMessage(c4d.COREMSG_CINEMA, bc, 0)
return True
def GenerateDragData(self, doc: c4d.documents.BaseDocument, dragType
) -> str | c4d.BaseMaterial | maxon.DragAndDropDataAssetArray:
"""Generates the drag data for the given drag type.
Each tile just encapsulates a file path, but we realize dragging them as file paths,
materials, or assets. So, when the type is material or asset, the drag data must be
generated from the file path. Which is why we have this method here.
"""
if not c4d.threading.GeIsMainThread():
raise RuntimeError(
"GenerateDragData must be called from the main thread.")
# The user has selected "File" as the drag type, we just return the file path.
if dragType == c4d.DRAGTYPE_FILENAME_IMAGE:
return self._path
# The user has selected "Material" as the drag type, we create a Redshift material with the
# texture as input and return that material.
elif dragType == c4d.DRAGTYPE_ATOMARRAY:
material: c4d.BaseMaterial = mxutils.CheckType(
c4d.BaseMaterial(c4d.Mmaterial))
# Create a simple graph using that texture, and insert the material into the
# active document.
maxon.GraphDescription.ApplyDescription(
maxon.GraphDescription.GetGraph(
material, maxon.NodeSpaceIdentifiers.RedshiftMaterial),
{
"$type": "Output",
"Surface": {
"$type": "Standard Material",
"Base/Color": {
"$type": "Texture",
# Set the texture. When our source is a file we will just have a plain
# path, e.g. "C:/path/to/file.png" and we have to prefix with a scheme
# (file) to make it a valid URL. When we are dealing with an asset, the
# texture path will already be a valid URL in the "asset:///" scheme.
"Image/Filename/Path": (maxon.Url(f"{self._path}")
if RE_URL_HAS_SCHEME.match(self._path) else
maxon.Url(f"file:///{self._path}"))
}
}
})
doc.InsertMaterial(material)
return [material]
# The user has selected "Asset" as the drag type, we create a texture asset and return that.
elif dragType == maxon.DRAGTYPE_ASSET:
# So we have to cases here, either that we want truly want to generate an asset drag
# event or that we just want to piggy-back onto the texture asset drag and drop
# mechanism of Cinema 4D, which is what we do here.
# Code for truly generating an asset drag event, where we would store the asset in the
# scene repository.
generateAssets: bool = False
if generateAssets:
# Get the scene repository and create an asset storage structure for the asset.
repo: maxon.AssetRepository = doc.GetSceneRepository(True)
store: maxon.StoreAssetStruct = maxon.StoreAssetStruct(
maxon.Id(), repo, repo)
# Now save the texture asset to the repository.
url: maxon.Url = maxon.Url(self._path)
name: str = url.GetName()
asset, _ = maxon.AssetCreationInterface.SaveTextureAsset(
url, name, store, (), True)
# Create a drag array for that asset. It is important that we use the URL of the
# asset (maxon.AssetInterface.GetAssetUrl) and not #url or asset.GetUrl(), as both
# are physical file paths, and will then cause the drag and drop handling to use
# these physical files, including the popup asking for wether the file should be
# copied. We will exploit exactly this behavior in the second case.
dragArray: maxon.DragAndDropDataAssetArray = maxon.DragAndDropDataAssetArray()
dragArray.SetLookupRepository(repo)
dragArray.SetAssetDescriptions((
(asset, maxon.AssetInterface.GetAssetUrl(asset, True), maxon.String(name)),
))
return dragArray
# With this case we can piggy-back onto the existing drag and drop texture asset
# handling of Cinema 4D, without actually having to create an asset. THIS IS A
# WORKAROUND, we could decide at any moment to change how the drag and drop
# handling of assets works, possibly rendering this approach obsolete.
else:
# The slight disadvantage of this approach (besides it being a hack and us possibly
# removing it at some point) is that we have to pay the download cost of
# #self._host._dummyAssetId once. I.e., the user has to download that texture once
# either by using it in the Asset Browser or by running this code. Once the asset has
# been cached, this will not happen again.
#
# But since we search below in a 'whoever comes first' manner, what is the first
# texture asset could change, and this could then be an asset which has not been
# downloaded yet. So, in a more robust world we would pick a fixed asset below. But
# this comes then with the burden of maintenance, as we could remove one of the
# builtin texture assets at any time. So, the super advanced solution would be to
# ship the plugin with its own asset database, and mount and use that. Here we could
# use a hard-coded asset ID, and would have to have never pay download costs, as
# the repository would be local.
# Find the asset by its Id, which we have stored in the dialog.
repo: maxon.AssetRepositoryInterface = maxon.AssetInterface.GetUserPrefsRepository()
dummy: maxon.AssetDescription = repo.FindLatestAsset(
maxon.AssetTypes.File().GetId(), self._host._dummyAssetId, maxon.Id(),
maxon.ASSET_FIND_MODE.LATEST)
# Setup the drag array with the asset. Note that #asset must be a valid texture
# asset, so just passing maxon.AssetDescription() will not work. But the backend
# for drag and drop handling will actually ignore the asset except for type checking
# it, and use the URL we provided instead.
dragArray: maxon.DragAndDropDataAssetArray = maxon.DragAndDropDataAssetArray()
dragArray.SetLookupRepository(repo)
dragArray.SetAssetDescriptions((
(dummy, maxon.Url(self._path), maxon.String(os.path.basename(self._path))),
))
return dragArray
else:
raise ValueError(f"Unsupported drag type: {dragType}")
def RemoveDragData(self, doc: c4d.documents.BaseDocument,
data: str | list[c4d.BaseMaterial] | maxon.DragAndDropDataAssetArray) -> bool:
"""Removes generated content when the user cancels a drag event.
Sometimes the user starts a drag event, but then cancels it, e.g., by pressing the
escape key. In this case, we have to remove the generated content, e.g., the material or
asset that we created for the drag event.
"""
if not c4d.threading.GeIsMainThread():
return False
if isinstance(data, list):
# Remove the dragged materials from the document. Since we are always generating a new
# material, we can just remove them. That is in general probably not the best design,
# and a real world application should avoid duplicating materials.
for item in data:
if isinstance(item, c4d.BaseList2D):
item.Remove()
elif isinstance(data, str):
# Here we could technically remove a file.
pass
elif isinstance(data, maxon.DragAndDropDataAssetArray):
# We could remove the asset, but other than for the material, there is no guarantee
# that we are the only user of it, as the Asset API has a duplicate preventing
# mechanism. So, the #SaveTextureAsset call above could have returned an already
# existing asset. Since we could operate with a document bound repository, we could
# search the whole document for asset references and only when we find none, remove
# the asset.
#
# We use here the little hack that we actually do not create an asset, to just to piggy-
# back onto the material drag and drop mechanism of assets, so we can just ignore
# this case.
pass
return True
class BitmapStackDialog(c4d.gui.GeDialog):
"""Implements a a simple dialog that stacks multiple BitmapCard controls.
"""
ID_DRAG_TYPE: int = 1002
def __init__(self):
"""Constructs a new ExampleDialog object.
"""
# The user area controls that will generate and receive drag events.
self._cards: list[BitmapCard] = [
BitmapCard(host=self) for _ in range(4)]
# The id for the dummy asset that used to generate 'fake' texture asset drag events. We
# search for it here once, so that we do not have to do it in the __init__ of each
# BitmapCard, or even worse, in the HandleDragEvent of each BitmapCard.
# We could technically also store here the AssetDescription instead of the Id, but that
# reference can go stale, so we just store the Id and then grab with it the asset when we
# need it. Which is much faster than searching asset broadly as we do here. In the end, we
# could probably also do all this in the drag handling of the BitmapCard, but a little bit
# of optimization does not hurt.
self._dummyAssetId: maxon.Id | None = None
if not maxon.AssetDataBasesInterface.WaitForDatabaseLoading():
raise RuntimeError("Could not initialize the asset databases.")
def findTexture(asset: maxon.AssetDescription) -> bool:
"""Finds the first texture asset in the user preferences repository.
"""
# When this is not a texture asset, we just continue searching.
meta: maxon.AssetMetaData = asset.GetMetaData()
if (meta.Get(maxon.ASSETMETADATA.SubType, maxon.Id()) !=
maxon.ASSETMETADATA.SubType_ENUM_MediaImage):
return True
# When it is a texture asset, we store it as the dummy asset and stop searching.
self._dummyAssetId = asset.GetId()
return False
# Search for our dummy asset in the user preferences repository.
repo: maxon.AssetRepositoryInterface = maxon.AssetInterface.GetUserPrefsRepository()
repo.FindAssets(maxon.AssetTypes.File().GetId(), maxon.Id(), maxon.Id(),
maxon.ASSET_FIND_MODE.LATEST, findTexture)
def CreateLayout(self):
"""Called by Cinema 4D to populate the dialog with controls.
"""
self.SetTitle("Drag and Drop Example")
self.GroupSpace(5, 5)
self.GroupBorderSpace(5, 5, 5, 5)
# Build the combo box in the menu bar of the dialog.
self.GroupBeginInMenuLine()
self.GroupBegin(1000, c4d.BFH_LEFT | c4d.BFV_TOP, cols=2)
self.GroupBorderSpace(5, 5, 5, 5)
self.GroupSpace(5, 5)
self.AddStaticText(1001, c4d.BFH_LEFT |
c4d.BFV_CENTER, name="Drag Events as:")
self.AddComboBox(1002, c4d.BFH_RIGHT | c4d.BFV_TOP)
self.GroupEnd()
self.GroupEnd()
# Add the BitmapCard controls to the dialog.
for i, card in enumerate(self._cards):
self.AddUserArea(2000 + i, c4d.BFH_LEFT | c4d.BFV_TOP)
self.AttachUserArea(card, 2000 + i)
# Add items to the combo box to select the drag type.
self.AddChild(self.ID_DRAG_TYPE, c4d.DRAGTYPE_FILENAME_IMAGE, "Files")
self.AddChild(self.ID_DRAG_TYPE, c4d.DRAGTYPE_ATOMARRAY, "Materials")
self.AddChild(self.ID_DRAG_TYPE, maxon.DRAGTYPE_ASSET, "Assets")
self.SetInt32(self.ID_DRAG_TYPE, c4d.DRAGTYPE_FILENAME_IMAGE)
return True
# Define a global variable of the dialog to keep it alive in a Script Manager script. ASYNC dialogs
# should not be opened in production code from a Script Manager script, as this results in a
# dangling dialog. Implement a command plugin when you need async dialogs in production.
dlg: BitmapStackDialog = BitmapStackDialog()
if __name__ == "__main__":
dlg.Open(dlgtype=c4d.DLG_TYPE_ASYNC, defaultw=150, defaulth=250)
Hi,
that your script is not working has not anything to do with pseudo decimals
, but the fact that you are treating numbers as strings (which is generally a bad idea) in a not very careful manner. When you truncate the string representation of a number which is represented in scientific notation (with an exponent), then you also truncate that exponent and therefor change the value of the number.
To truncate a float
you can either take the floor
of my_float * 10 ** digits
and then divide by 10 ** digits
again or use the keyword round
.
data = [0.03659665587738824,
0.00018878623163019122,
1.1076812650509394e-03,
1.3882258325566638e-06]
for n in data:
rounded = round(n, 4)
floored = int(n * 10000) / 10000
print(n, rounded, floored)
0.03659665587738824 0.0366 0.0365
0.00018878623163019122 0.0002 0.0001
0.0011076812650509394 0.0011 0.0011
1.3882258325566637e-06 0.0 0.0
[Finished in 0.1s]
Cheers
zipit
Dear community,
We will have to touch multiple parts of developers.maxon.net
on the 18.01.2024 and 19.01.2024 22.01.2024. This will result in outages of our documentation and the forum these days. I will try to keep the outage times to a minimum and it will certainly not span the whole two days. But especially one task I will do on Friday might take hours to complete and I can only do that on a forum which is in maintenance mode.
Please make sure to download a recent offline documentation in case you plan to do extended development work the next two days. As a result, forum support might also be delayed on these days.
Cheers,
Ferdinand
Hi,
as @Cairyn said the problem is unreachable code. I also just saw now that you did assign the same ID to all your buttons in your CreateLayout()
. Ressource and dialog element IDs should be unique. I would generally recommend to define your dialogs using a resource, but here is an example on how to do it in code.
BUTTON_BASE_ID = 1000
BUTTON_NAMES = ["Button1", "Button2", "Button3", "Button4", "Button5"]
BUTTON_DATA = {BUTTON_BASE_ID + i: name for i, name in enumerate(BUTTON_NAMES)}
class MyDialog(gui.GeDialog):
def CreateLayout(self):
"""
"""
self.GroupBegin(id=1013, flags=c4d.BFH_SCALEFIT, cols=5, rows=4)
for element_id, element_name in BUTTON_DATA.items():
self.AddButton(element_id, c4d.BFV_MASK, initw=100,
name=element_name)
self.GroupEnd()
return True
def Command(self, id, msg):
"""
"""
if id == BUTTON_BASE_ID:
print "First button has been clicked"
elif id == BUTTON_BASE_ID + 1:
print "Second button has been clicked"
# ...
if id in BUTTON_DATA.keys(): # or just if id in BUTTON_DATA
self.Close()
return True
Dear development community,
On September the 10th, 2024, Maxon Computer released Cinema 4D 2025.0.0. For an overview of the new features of Cinema 4D 2025.0, please refer to the release announcement. Alongside this release, a new Cinema 4D SDK and SDK documentation have been released, reflecting the API changes for 2025.0.0. The major changes are:
cinema
namespace has been introduced which contains all the entities which were formerly in the anonymous global namespace known as the Classic API. Plugin authors must adopt their code to this new API, although the changes are not nearly as extensive as for 2024. See the 2025 migration guide for details. Code examples and documentation have been updated to now refer to a Cinema API.c4d
package remains the home for all formerly Classic and now Cinema API entities.Head to our download section for the newest SDK downloads, or the C++ and Python API change notes for an in detail overview of the changes.
We discovered late in the cycle bugs in the Asset API code examples and OCIO code in the Python SDK. Which is why the publication of the Python SDK and GitHub code examples has been postponed until these bugs are fixed. They should be ready latest by Friday the 13th of September. But the Python online documentation is accessible and error free (to our knowledge).
We had to make some last minute changes to the C++ SDK regarding OCIO code examples. Only the extended C++ SDK contains these changes. The application provided
sdk.zip
will catch up with the next release of Cinema 4D.
Happy rendering and coding,
the Maxon SDK Team
Cloudflare unfortunately still does interfere with our server cache. And you might have to refresh your cache manually.
When you are not automatically redirected to the new versions, and also do not see 2024.5 in the version selector, please press
CTRL + F5
or pressCTRL
and click on the reload icon of your browser anywhere ondevelopers.maxon.net/docs/
to refresh your cache. You only have to do this once and it will apply to all documentations at once. Otherwise your cache will automatically update latest by 19/07/2024 00:00.
Hi,
sorry for all the confusion. You have to pass actual instances of objects. The following code does what you want (and this time I actually tried it myself ;)).
import c4d
def main():
"""
"""
bc = doc.GetAllTextures(ar=doc.GetMaterials())
for cid, value in bc:
print cid, value
if __name__=='__main__':
main()
Cheers,
zipit
Dear development community,
On June the 18th, 2025, Maxon Computer released Cinema 4D 2025.3.0. For an overview of the new features of Cinema 4D 2025.3.0, please refer to the 2025.3.0 release notes. Alongside this release, a new Cinema 4D SDK and SDK documentation have been released, reflecting the API changes for 2025.3.0. The major changes are:
In 2025.3.0, there is a critical issue with
c4dpy
that will cause it to halt indefinitely when run for the first time. See the c4dpy manual for an explanation and workaround. This issue will be fixed with a hotfix for 2025.3.0.
c4d.documents.BatchRender
class, to have more control over render settings, cameras, and takes for a given job.Head to our download section to grab the newest SDK downloads, or read the C++ or Python API change notes for an in detail overview of the changes.
Happy rendering and coding,
the Maxon SDK Team
Cloudflare unfortunately still does interfere with our server cache. You might have to refresh your cache manually to see new data when you read this posting within 24 hours of its release.
Hey,
I now know how our API will look like in this regard in 2026.0.0. Its compile/runtime behaviour will be as for 2025.2.1 and before. And we will introduce a new flag NOHANDLEFOCUS
which must be set when attaching the area when one explicitly wants to ignore focus events. So, you will not have to change your code when you were happy with your user area behaviour as it is has been.
Cheers,
Ferdinand
Hey @Aprecigout,
helps me avoid saying more “silly things.”
You cannot make an omelette without breaking eggs, so no worries. There are no silly questions.
To give you even more context on why we lean so heavily on ExecutePasses: our artists make massive use of MoGraph ...
Thank you for the details. This gives a bit more insight. But without a concrete scene, it is still very hard to evaluate for me where this comes from (if there is a bug somewhere). As hinted at in my last posting, the Python VM is a rather unlikely suspect for a cause. But the alternative would be that our scene evaluation is massively bugged which is even more unlikely. But it is good to know that C++ is an option for you. In general, I am still not convinced that your findings, that Cinema 4D irregularly slows down on scene execution, are correct. Are you sure that you unload the documents between runs? I.e., something like this shown in [1]? Because if you do not, you will of course occupy more and more memory with each document loaded.
Right now, ExecutePasses is the only way I know to retrieve per-frame information for every clone.
This is also a bit ambiguous, but this might be wrong. Executing the passes does not necessarily mean a document will build discrete data for each clone or everything in general. It will just build what it deems necessary. When a MoGraph cloner is in 'Multi-Instance' mode it will actually only build the first clone concretely, the rest of the clones is still being described non-discretely (i.e., sparely) via the MoData
tag (so that it realizes the memory saving aspect this mode promises). You can read this thread, it might contain relevant information for you.
When you want a super flattened document, you could invoke Save for Cineware from the app, or SaveDocument
with the SAVEDOCUMENTFLAGS
flag SAVECACHES
from the API to save a document with exhaustively build caches. But once you load such document into any form of Cinema 4D (e.g., Cinema 4D
, Commandline
, c4dpy
, etc. ) it will throw away all these caches and switch back to an optimized scene model. To faithfully read such c4d
export document, you must use the Cineware AP. Which is C++ only and not always trivial in its details. But when you are comfortable with C++, and you need a truly discretely serialized document, this is the way to go. Just to be verbose: With the Cineware API you can ONLY read discrete data. Anything dynamic/computed is not available here. Because you can use the Cineware API without a Cinema 4D installation. So, executing the passes is for example not possible here. And not all aspects of a scene can be exported into this format. But MoGraph systems will be exported.
Cheers,
Ferdinand
[1]
"""A simple example for how to unload documents and execute passes in a thread.
This is pseudo code I wrote blindly and did not run.
"""
import c4d
import time
doc: c4d.documents.BaseDocument # A preloaded document, could be None.
class PassesThread (c4d.threading.C4DThread):
"""Executes the passes on a document in a thread.
Using this inside a plain Script Manager makes no sense, since it is itself blocking. You need
something asynchronous such as an async dialog for this to be useful.
"""
def __init__(self, doc: c4d.documents.BaseDocument) -> None:
self._doc = doc
self._result: bool = False
self.Start()
def Main(self) -> None:
self._result = self._doc.ExecutePasses(
self.Get(), True, True, True, c4d.BUILDFLAGS_NONE)
def main() -> None:
"""Called by Cinema 4D to execute the script.
"""
# The file paths of the documents to load and execute, could also be the same file over and over again.
for fPath in (...):
# Kill the current document to free up resources and load the new one.
if isinstance(doc, c4d.documents.BaseDocument):
c4d.documents.KillDocument(doc)
doc: c4d.documents.BaseDocument = c4d.documents.LoadDocument(fPath, ...)
frames: list[c4d.BaseTime] = [...]
for f in frames:
doc.SetTime(f)
# Create a thread to execute the passes, which makes no sense here since we will wait for
# the outcome anyway, but it shows the principle. But you can in any case always only run
# of these threads at a time.
thread: PassesThread = PassesThread(doc)
while thread.IsRunning():
time.sleep(1.0) # Avoid blasting the thread with finish checks.
# For the final frame, we should execute the passes again, so that simulations can settle.
thread: PassesThread = PassesThread(doc)
while thread.IsRunning():
time. Sleep(1.0)
Hey @Aprecigout,
Welcome to the Maxon developers forum and its community, it is great to have you with us!
Before creating your next postings, we would recommend making yourself accustomed with our forum and support procedures. You did not do anything wrong, we point all new users to these rules.
It is strongly recommended to read the first two topics carefully, especially the section Support Procedures: How to Ask Questions.
Please have a look at Support Procedures: How to Ask Questions, there are too many question here. Which often derails threads, but since you are new, let's try it like this.
Is there a recommended way to flush or bypass the cache/memory buildup that seems to happen inside ExecutePasses, so execution time stays consistent?
Caches cannot be flushed, as they are a mandatory part of the scene graph. A scene always has exactly one cache per scene element which requires a cache. Without it is not operational. And while the term is effectively correct, the scene graph cache is not what you usually picture when talking about caches. Caches are the discrete embodiment of parametric objects and deformers. You can think of them as an invisible part of the scene hierarchy. With the 'Active Object' plugin from the C++ SDK we can have a peek at what cache means. Here we unfold the first cube object clone of the cloner in the cache.
In the next release of Cinema 4D there will also be mxutils.GetSceneGraphString
which has a similar purpose as the the plugin from the C++ SDK. For the scene shown above it will print what is shown in [1]. Everything below the [Cache]
of Cloner
is the cache of that scene element; a rather complex hidden hierarchy which itself contains caches which must be unpacked.
This also hints at the non-linear nature of caches. When you have a scene with 100 frames, where frame 0 is literally the empty scene and on frame 100 you have thousands of high resolution parametric objects which have to be rebuilt for this frame, and on top of that multiple complex simulations (pyro, particles, liquids), then executing the passes for frame 0
will be very quick as there is literally nothing to do, while executing the passes for frame 100
could cost seconds or even minutes (when none of the simulations are cached).
If such mechanisms exist, could someone outline the usual workflow or API calls to use—or point me to the relevant documentation?
Without knowing what you want to do, that is impossible to answer. Your code there could be slightly incorrect, as for 'pre-rolling' you usually want to execute the last pass twice, so that simulations can settle. When you just want to step through a scene, what you are doing is okay. It also should not make a big difference if your execute the passes for a scene state once ot twice, as all scene elements should only rebuild its caches when necessary when asked to do so. So, when you for example excute the passes and it takes 60 seconds, and then do it right again, the second run should only take a fraction of the first execution, as most scene elments should see that they are not dirty anymore, and just return their already existing cache.
But in general it is a bit odd that you execute the passes on all frames. It is very rare that you have to do that from Python. Maybe you could explain why you are doing that?
Finally, could C4DThread help in this context, or am I barking up the wrong tree? My experiments based on the thread linked above haven’t produced conclusive results.
The first argument of ExecutePasses
is the thread to which the call shall be bound. You can use this to make the pass execution non-blocking for the main thread.
This also hints at the broader answer. The pass execution is of course already heavily optimized and runs in as many threads as the machine can muster and things such as ObjectData::GetVirtualObjects
which are the backbone of cache building are run massively in parallel. The only thing you can decide is if you want to make your call non-blocking for the main thread or not (where the GUI code runs).
Not explicitly asked but sort of the elephant in the room: Executing the passes for all frames of a document varies drastically.
Just like many modern DCCs, Cinema 4D has a pretty sophisticated backed. Cinema 4D has for example on top of the "caching" of the scene graph a memoization core, which records the results of previous computations and reuses them when the same data is requested again. There is naturally some variance in such complex systems, where tiny changes in the input conditions can lead to significant differences in the execution time.
But what you show us there, that executing all passes of a scene takes twice or three times as long as the first time, is not normal. But I would at first be a bit doubtful that your findings are correct, as this would hint at a massive bug in the pass execution system. There could be an issue with the Python VM. I would recommend to unload the document in between the runs, to ensure that all possible memory is really freed.
Executing the passes for a single frame is already a not cheap operation, executing the passes for all frames of a document can be extensively expensive, since scene initialization makes up a good chunk of the render time of a document. So, doing this more than once in a row, is not the most clever thing. When you want to do this on multiple documents in a row, you should of course unload documents you are done with, so that you can free the memory.
Cheers,
Ferdinand
[1] Using print(mxutils.GetSceneGraphString(doc))
to visualize the content of a scene. Since we pass the whole document and not just the cloner, really everything gets unpacked here. Everything below the [Cache]
child of 'Cloner' (BaseObject: Omgcloner)
is the cache of the cloner object.
'' (BaseDocument: Tbasedocument)
├── [Branch] 'Objects' (Obase)
│ └── 'Cloner' (BaseObject: Omgcloner)
│ ├── [Cache]
│ │ └── 'Null' (BaseObject: Onull)
│ │ ├── 'Cube 0' (BaseObject: Ocube)
│ │ │ ├── [Cache]
│ │ │ │ └── 'Cube 0' (PolygonObject: Opolygon)
│ │ │ │ ├── [Deform Cache]
│ │ │ │ │ └── 'Cube 0' (PolygonObject: Opolygon)
│ │ │ │ │ └── [Branch] 'Tags' (Tbase)
│ │ │ │ │ ├── 'Phong' (BaseTag: Tphong)
│ │ │ │ │ ├── 'UVW' (UVWTag: Tuvw)
│ │ │ │ │ ├── '' (PolygonTag: Tpolygon)
│ │ │ │ │ └── '' (PointTag: Tpoint)
│ │ │ │ └── [Branch] 'Tags' (Tbase)
│ │ │ │ ├── 'Phong' (BaseTag: Tphong)
│ │ │ │ ├── 'UVW' (UVWTag: Tuvw)
│ │ │ │ ├── '' (PolygonTag: Tpolygon)
│ │ │ │ └── '' (PointTag: Tpoint)
│ │ │ ├── [Branch] 'Tags' (Tbase)
│ │ │ │ ├── 'Motion Graphics Color Tag' (BaseTag: Tmgcolor)
│ │ │ │ └── 'Phong' (BaseTag: Tphong)
│ │ │ └── 'Bend' (BaseObject: Obend)
│ │ ├── 'Sphere 1' (BaseObject: Osphere)
│ │ │ ├── [Cache]
│ │ │ │ └── 'Sphere 1' (PolygonObject: Opolygon)
│ │ │ │ ├── [Deform Cache]
│ │ │ │ │ └── 'Sphere 1' (PolygonObject: Opolygon)
│ │ │ │ │ └── [Branch] 'Tags' (Tbase)
│ │ │ │ │ ├── 'Phong' (BaseTag: Tphong)
│ │ │ │ │ ├── 'UVW' (UVWTag: Tuvw)
│ │ │ │ │ ├── '' (PolygonTag: Tpolygon)
│ │ │ │ │ └── '' (PointTag: Tpoint)
│ │ │ │ └── [Branch] 'Tags' (Tbase)
│ │ │ │ ├── 'Phong' (BaseTag: Tphong)
│ │ │ │ ├── 'UVW' (UVWTag: Tuvw)
│ │ │ │ ├── '' (PolygonTag: Tpolygon)
│ │ │ │ └── '' (PointTag: Tpoint)
│ │ │ ├── [Branch] 'Tags' (Tbase)
│ │ │ │ ├── 'Motion Graphics Color Tag' (BaseTag: Tmgcolor)
│ │ │ │ └── 'Phong' (BaseTag: Tphong)
│ │ │ └── 'Bend' (BaseObject: Obend)
│ │ ├── 'Cube 2' (BaseObject: Ocube)
│ │ │ ├── [Cache]
│ │ │ │ └── 'Cube 2' (PolygonObject: Opolygon)
│ │ │ │ ├── [Deform Cache]
│ │ │ │ │ └── 'Cube 2' (PolygonObject: Opolygon)
│ │ │ │ │ └── [Branch] 'Tags' (Tbase)
│ │ │ │ │ ├── 'Phong' (BaseTag: Tphong)
│ │ │ │ │ ├── 'UVW' (UVWTag: Tuvw)
│ │ │ │ │ ├── '' (PolygonTag: Tpolygon)
│ │ │ │ │ └── '' (PointTag: Tpoint)
│ │ │ │ └── [Branch] 'Tags' (Tbase)
│ │ │ │ ├── 'Phong' (BaseTag: Tphong)
│ │ │ │ ├── 'UVW' (UVWTag: Tuvw)
│ │ │ │ ├── '' (PolygonTag: Tpolygon)
│ │ │ │ └── '' (PointTag: Tpoint)
│ │ │ ├── [Branch] 'Tags' (Tbase)
│ │ │ │ ├── 'Motion Graphics Color Tag' (BaseTag: Tmgcolor)
│ │ │ │ └── 'Phong' (BaseTag: Tphong)
│ │ │ └── 'Bend' (BaseObject: Obend)
│ │ ├── 'Sphere 3' (BaseObject: Osphere)
│ │ │ ├── [Cache]
│ │ │ │ └── 'Sphere 3' (PolygonObject: Opolygon)
│ │ │ │ ├── [Deform Cache]
│ │ │ │ │ └── 'Sphere 3' (PolygonObject: Opolygon)
│ │ │ │ │ └── [Branch] 'Tags' (Tbase)
│ │ │ │ │ ├── 'Phong' (BaseTag: Tphong)
│ │ │ │ │ ├── 'UVW' (UVWTag: Tuvw)
│ │ │ │ │ ├── '' (PolygonTag: Tpolygon)
│ │ │ │ │ └── '' (PointTag: Tpoint)
│ │ │ │ └── [Branch] 'Tags' (Tbase)
│ │ │ │ ├── 'Phong' (BaseTag: Tphong)
│ │ │ │ ├── 'UVW' (UVWTag: Tuvw)
│ │ │ │ ├── '' (PolygonTag: Tpolygon)
│ │ │ │ └── '' (PointTag: Tpoint)
│ │ │ ├── [Branch] 'Tags' (Tbase)
│ │ │ │ ├── 'Motion Graphics Color Tag' (BaseTag: Tmgcolor)
│ │ │ │ └── 'Phong' (BaseTag: Tphong)
│ │ │ └── 'Bend' (BaseObject: Obend)
│ │ └── 'Cube 4' (BaseObject: Ocube)
│ │ ├── [Cache]
│ │ │ └── 'Cube 4' (PolygonObject: Opolygon)
│ │ │ ├── [Deform Cache]
│ │ │ │ └── 'Cube 4' (PolygonObject: Opolygon)
│ │ │ │ └── [Branch] 'Tags' (Tbase)
│ │ │ │ ├── 'Phong' (BaseTag: Tphong)
│ │ │ │ ├── 'UVW' (UVWTag: Tuvw)
│ │ │ │ ├── '' (PolygonTag: Tpolygon)
│ │ │ │ └── '' (PointTag: Tpoint)
│ │ │ └── [Branch] 'Tags' (Tbase)
│ │ │ ├── 'Phong' (BaseTag: Tphong)
│ │ │ ├── 'UVW' (UVWTag: Tuvw)
│ │ │ ├── '' (PolygonTag: Tpolygon)
│ │ │ └── '' (PointTag: Tpoint)
│ │ ├── [Branch] 'Tags' (Tbase)
│ │ │ ├── 'Motion Graphics Color Tag' (BaseTag: Tmgcolor)
│ │ │ └── 'Phong' (BaseTag: Tphong)
│ │ └── 'Bend' (BaseObject: Obend)
│ ├── [Branch] 'Tags' (Tbase)
│ │ └── 'Info' (BaseTag: ID_MOTAGDATA)
│ ├── 'Cube' (BaseObject: Ocube)
│ │ ├── [Branch] 'Tags' (Tbase)
│ │ │ └── 'Phong' (BaseTag: Tphong)
│ │ └── 'Bend' (BaseObject: Obend)
│ └── 'Sphere' (BaseObject: Osphere)
│ ├── [Branch] 'Tags' (Tbase)
│ │ └── 'Phong' (BaseTag: Tphong)
│ └── 'Bend' (BaseObject: Obend)
├── [Branch] 'Render Settings' (Rbase)
│ └── 'My Render Setting' (RenderData: Rbase)
│ ├── [Branch] 'Post Effects' (VPbase)
│ │ ├── 'Magic Bullet Looks' (BaseVideoPost: VPMagicBulletLooks)
│ │ └── 'Redshift' (BaseVideoPost: VPrsrenderer)
│ └── [Branch] 'Multi-Pass' (Zmultipass)
│ └── 'Post Effects' (BaseList2D: Zmultipass)
├── [Branch] 'Scene Hooks' (SHplugin)
│ ├── 'STHOOK' (BaseList2D: 1012061)
│ ├── 'RSCameraObjectTargetDistancePicker' (BaseList2D: 31028063)
│ ├── 'Python Embedded Change Monitor' (BaseList2D: 1058422)
│ ├── 'SceneHook' (BaseList2D: 1028481)
│ ├── 'CmSceneHook' (BaseList2D: 1026839)
│ ├── 'CameraMorphDrawSceneHook' (BaseList2D: 1029281)
│ ├── 'MotionCameraDrawSceneHook' (BaseList2D: 1029338)
│ ├── 'USD Scene Hook' (BaseList2D: 1055307)
│ ├── 'Substance Assets' (BaseList2D: 1032107)
│ ├── 'Alembic Archive Hook' (BaseList2D: 1028458)
│ ├── 'UpdateMerge Hook' (BaseList2D: 465001602)
│ ├── 'ArchiExchangeCADHook' (BaseList2D: 200000216)
│ ├── 'SLA wave scene hook' (BaseList2D: REG_EXP_PARSER)
│ ├── 'Thinking Particles' (TP_MasterSystem: ID_THINKINGPARTICLES)
│ ├── '' (BaseList2D: 1035577)
│ ├── 'Bullet' (BaseList2D: 180000100)
│ ├── 'XRefs' (BaseList2D: 1025807)
│ ├── 'CAManagerHook' (BaseList2D: 1019636)
│ │ └── [Branch] 'Weights Handler Head' (Tbaselist2d)
│ │ └── 'Weights Handler' (BaseList2D: 1037891)
│ ├── 'Volume Save Manager Hook' (BaseList2D: 1040459)
│ ├── 'UV Display 3D SceneHook' (BaseList2D: 1054166)
│ ├── 'uvhook' (BaseList2D: 1053309)
│ ├── 'ScatterPlacementHook' (BaseList2D: 1058060)
│ ├── 'Tool System Hook' (BaseList2D: ID_TOOL_SYSTEM_HOOK)
│ │ └── [Branch] 'SBM' (431000215)
│ │ └── 'Symmetry node' (BaseList2D: 431000215)
│ │ └── [Branch] 'C4DCoreWrapper' (200001044)
│ │ └── 'Symmetry node - net.maxon.symmetry.context.modeling' (BaseList2D: 300001078)
│ ├── 'MoGraphSceneHook' (BaseList2D: 1019525)
│ ├── 'gozScenehook' (BaseList2D: 1059748)
│ ├── 'Simulation' (BaseList2D: ID_SIMULATIONSCENE_HOOK)
│ │ └── [Branch] 'Simulation World' (Obase)
│ │ └── 'Default Simulation Scene' (BaseObject: Osimulationscene)
│ ├── 'PersistentHook' (BaseList2D: 180420202)
│ ├── 'Scene Nodes' (BaseList2D: SCENENODES_IDS_SCENEHOOK_ID)
│ ├── 'NE_SceneHook' (BaseList2D: 465002367)
│ ├── 'Take Hook' (BaseList2D: 431000055)
│ │ └── [Branch] 'Take System Branch' (TakeBase)
│ │ └── 'Main' (BaseTake: TakeBase)
│ │ └── [Branch] 'Override Folders' (431000073)
│ │ └── 'Overrides' (BaseList2D: 431000073)
│ │ ├── 'Others' (BaseList2D: 431000073)
│ │ ├── 'Layers' (BaseList2D: 431000073)
│ │ ├── 'Materials' (BaseList2D: 431000073)
│ │ ├── 'Shaders' (BaseList2D: 431000073)
│ │ ├── 'Tags' (BaseList2D: 431000073)
│ │ └── 'Objects' (BaseList2D: 431000073)
│ ├── 'CombineAc18_AutoCombine_SceneHook' (BaseList2D: 1032178)
│ ├── 'PLKHUD' (BaseList2D: 1020132)
│ │ └── [Branch] 'PSUNDOHEAD' (Obase)
│ │ └── 'PKHOP' (BaseObject: 1020120)
│ ├── 'RenderManager Hook' (BaseList2D: 465003509)
│ ├── 'Sound Scrubbing Hook' (BaseList2D: 100004815)
│ ├── 'To Do' (BaseList2D: 465001536)
│ ├── 'Animation' (BaseList2D: 465001535)
│ ├── 'BaseSettings Hook' (BaseList2D: ID_BS_HOOK)
│ ├── '' (BaseList2D: 1060457)
│ ├── 'SculptBrushModifierSceneHook' (BaseList2D: 1030499)
│ ├── 'Sculpt Objects' (BaseList2D: 1024182)
│ ├── 'HairHighlightHook' (BaseList2D: 1018870)
│ ├── 'MeshObject Scene Hook' (BaseList2D: 1037041)
│ ├── 'Lod Hook' (BaseList2D: 431000182)
│ ├── 'Annotation Tag SceneHook' (BaseList2D: 1030679)
│ ├── 'Sniper' (BaseList2D: 430000000)
│ ├── 'Mesh Check Hook' (BaseList2D: 431000027)
│ ├── 'Modeling Objects Hook' (BaseList2D: 431000032)
│ │ └── [Branch] 'Modeling Objects Branch' (431000031)
│ │ ├── 'Pattern Direction Manipulator' (BaseObject: Opatternmanipulator)
│ │ ├── 'Plane Manipulator' (BaseObject: Oplanemanipulator)
│ │ ├── 'Pivot Manipulator' (BaseObject: Opivotmanipulator)
│ │ ├── 'Knife Line Manipulator' (BaseObject: 431000168)
│ │ ├── 'Subdivision Manipulator' (BaseObject: 431000172)
│ │ └── 'PolyPenObject' (BaseObject: 431000031)
│ ├── 'Snap Scenehook' (BaseList2D: 440000111)
│ │ ├── [Branch] 'WpSH' (440000111)
│ │ │ └── 'WorkPlane' (BaseObject: Oworkplane)
│ │ └── [Branch] 'MdSH' (Tbase)
│ │ └── 'Modeling Settings' (BaseList2D: 440000140)
│ ├── 'Doodle Hook' (BaseList2D: 1022212)
│ ├── 'Stereoscopic' (BaseList2D: 450000226)
│ ├── 'ViewportExtHookHUD' (BaseList2D: ID_VIEW_SCENEHOOKHUD)
│ ├── 'ViewportExtHookhighlight' (BaseList2D: ID_VIEW_SCENEHOOKHIGHLIGHT)
│ ├── 'MeasureSceneHook' (BaseList2D: ID_MEASURE_SCENEHOOK)
│ ├── 'Redshift' (BaseList2D: 1036748)
│ ├── 'GvHook' (BaseList2D: ID_SCENEHOOK_PLUGIN)
│ ├── 'Material Scene Hook' (BaseList2D: 300001077)
│ ├── 'TargetDistancePicker' (BaseList2D: 1028063)
│ └── 'BodyPaint SceneHook' (BaseList2D: 1036428)
└── [Branch] '' (Tbasedraw)
└── '' (BaseList2D: 110306)
Hey @lionlion44,
Thank you for reaching out to us. We cannot provide support on third party libraries (Octane). But, yes, in general you are on the right track. We have this C++ example, which I loosely translated to Python. The thing to do which you are missing, is to check if such VP already exists, as you otherwise can land in a world of hurt.
For everything else, you would have to talk with the Octane devs (of which some are here on this forum), if there are any special further steps to be taken for Octane.
Cheers,
Ferdinand
"""Provides an example for generically setting a render engine in Cinema 4D.
Note that there is no guarantee that every render engine has a video post node, and when it has one,
that it uses the same ID as the render engine. But it is highly conventional to implement a render
engine like this.
Derived from the C++ Example "Set Render Engine to Redshift":
https://developers.maxon.net/docs/cpp/2023_2/page_manual_redshift_rendrer.html
"""
import c4d
import mxutils
doc: c4d.documents.BaseDocument # The active Cinema 4D document.
def SetRenderEngine(doc: c4d.documents.BaseDocument, newEngineId: int, createsVideoPostNode: bool) -> bool:
"""Sets the render engine of the given document to the specified ID.
"""
# Make sure we are on the main thread, as we plan to modify the document and ensure that our
# inputs are what we think they are.
if not c4d.threading.GeIsMainThread():
raise RuntimeError("SetRenderEngine must be called from the main thread.")
mxutils.CheckType(doc, c4d.documents.BaseDocument)
mxutils.CheckType(newEngineId, int)
mxutils.CheckType(createsVideoPostNode, bool)
# Get the currently active render engine ID and get out if it matches the new one.
renderData: c4d.documents.RenderData = doc.GetActiveRenderData()
currentEngineId: int = renderData[c4d.RDATA_RENDERENGINE]
if currentEngineId == newEngineId:
print(f"Render engine {newEngineId} is already set, no changes made.")
return True
# Try to find a video post with the render engine ID. There is no absolute guarantee that every
# render engine either has a video post node or that is gives it the same ID as the render
# engine (but it is strongly conventional).
if createsVideoPostNode:
# Try to find an already existing video post node with the render engine ID.
node: c4d.documents.BaseVideoPost | None = renderData.GetFirstVideoPost()
while node:
if node.GetType() == newEngineId:
break
node = node.GetNext()
# There is no video post for the render engine, so we try to a new create one.
if not node:
try:
node: c4d.documents.BaseVideoPost = c4d.documents.BaseVideoPost(newEngineId)
renderData.InsertVideoPost(node)
except Exception as e:
raise RuntimeError(f"Failed to create video post node for render engine {newEngineId} ({e}).")
# Finally, we set the render engine ID in the render data.
renderData[c4d.RDATA_RENDERENGINE] = newEngineId
return True
def main() -> None:
"""Called by Cinema 4D to run the script.
"""
# Setting the standard render engine, here we do not have to create a video post node, since
# the standard renderer is one of the rare cases that does not have a dedicated video post.
SetRenderEngine(doc, newEngineId=c4d.RDATA_RENDERENGINE_STANDARD, createsVideoPostNode=False)
# Set Redshift as the render engine, which does have a video post node.
SetRenderEngine(doc, newEngineId=c4d.VPrsrenderer, createsVideoPostNode=True)
# Push an update event.
c4d.EventAdd()
if __name__ == "__main__":
main()
Hey @shir,
Thank you for reaching out to us. A Program Database (PDB) is a debug information format from Microsoft. It is comparable to the DWARF debug information format often used under Linux and macOS. However, unlike DWARF under Linux, where debug information is directly compiled into the binary, Microsoft chooses to store debug information in separate files, the pdb
files.
When you attach a debugger to a binary without any debug information, it will by default only see the machine code of the binary. So when you have an issue and the debugger puts out a stack trace, it will only show you the offsets in a library, e.g., something like this:
#1 0x0000000000767576 in myBinary.dll
#2 0x0000000000767df4 in otherBinary.dll
#3 0x0000000000773aca in myBinary.dll
#4 0x00000000004b893e in myBinary.dll
You can see this happen in the call stack window in your screenshot. VS only provides information in the format someBinary.ext!someAddress()
, e.g., c4d_base.xdl64!00007ffb200acfb7()
, as it has no further information. With bin!address()
VS means a function at that address is being called. In my opinion, VS has one of the most cryptic stack trace formats out there and can be a bit confusing for beginners.
To see meaningful output, you need the debug information for that binary, which among other things contains the mapping of addresses to source code. If you have the pdb
file for the binary, you can load it into your debugger, and it will then show you something like this instead:
#1 0x0000000000767576 in MyClass::MyMethod() at myClass.cpp:42
#2 0x0000000000767df4 in OtherClass::OtherMethod() at otherClass.cpp:15
#3 0x0000000000773aca in MyClass::AnotherMethod() at myClass.cpp:78
#4 0x00000000004b893e in main() at main.cpp:10
When you compile the Cinema 4D SDK and your source code, it will automatically generate the pdb
files for these binaries for you, so that you can debug them in a meaningful manner. But what we see here is Visual Studio asking you for the pdb
for c4d_base.xdl64
, one of the core library binaries located in the corelibs
folder of the Cinema 4D application you are debugging with. You did not compile that binary, so you do not have the pdb
file for it. And we do not ship our binaries with debug information, as that would not only be a very large download, but also would expose our source code to the public.
You are hitting a debug stop there (VS tells you that in the info box by stating this is a __debugbreak
). This is the less critical case of a debug event, which is covered by the very tutorial you are following (the other one being a critical stop). You can simply hit continue in your debugger and ignore this. The event seems to be raised from Redshift, judging by the stack trace we can see in the screenshot you provided. There is probably some minor hardware issue or so, and Redshift is trying to handle it gracefully by raising this debug event.
It is, however, not normal when this happens permanently and usually it hints at a corrupted installation of Cinema 4D or a hardware issue when you are always greeted by debug events on startup (or even when just running and interacting with Cinema 4D). Sometimes debug stops can happen as a one-time thing when you are debugging for the first time against some Cinema 4D instance (and it has not yet built all its prefs, caches, and other things Cinema 4D builds in the background). When this persists and you are annoyed by having to press continue, I would recommend trying to either remove Redshift from your Cinema 4D installation or reinstall Cinema 4D altogether.
You could also check inside of Cinema 4D if you can see any errors in the 'Redshift Feedback Display' window. For you as a third party, it is however not possible to find out what that issue in c4d_base.xdl64 at the offset 7ffb200acfb7 is.
Cheers,
Ferdinand
PS: There is also g_enableDebugBreak=true|false
which you can pass to your Cinema 4D instance as a commandline argument. With that you can permanently mute debug stops. But that is more of an expert feature and you probably do not want to enable that as a beginner.
Hey @shir,
good to hear that you solved the issue. Maybe NodeBB has an issue with the specific (top level) domain your mail handle was under? I just checked the logs and this is the event for the second registration mail that has been sent out (I edited your mail handle for privacy reasons). I.e., this is the one I manually invoked. There is another event for your actual registration. As far as NodeBB is concerned, it seems to be convinced that it successfully sent these mails.
{
"confirm_code": "dbcc0d6c-8646-4191-9975-badc1c7035f2",
"email": "[email protected]",
"subject": "Welcome to PluginCafé",
"template": "welcome",
"timestamp": 1751883962965
}
NodeBB can be a bit buggy from time to time but that it fails to send a mail and then creates an event for successfully sending it, would be a bit odd. I will have an eye on this.
Cheers,
Ferdinand
Hey @ECHekman,
I sense there is some frustration, but I am not sure telling us how bad our API is will get us anywhere. Yes, the Nodes API ist not trivial, but you are only on the using part (which is not that hard to understand) not the implementation part (which is the trickly one). There are multiple render engine vendors who took that hurdle. I already answered your questions, and as always you will not see source code from us, unless you give us executable code in the first place upon which we can build, or we deem a subject new.
I often bend these rules a bit where it makes sense to meet our customers and third parties halfway. But you cannot just throw a snippet at us and then expect us to invent everything around it and then fix that for you. Executable code makes a difference as lined out in our support procedures. My hunch would be that your getConnectedNode
does not work because you do not check if your nodes are valid.
You can get the value of a port with GetPortValue
or GetEffectivePortValue
. What you are doing with GetValue
and EffectivePortValue
is the old way but will still work.
// Redshift expresses a lot of its enums as strings and not as ints (did not check if that is here the case).
const String value = myPort.GetEffectivePortValue<String>().GetOrDefault() iferr_return;
And as lined out before, what your function is trying to do, can likely be done via GraphModelHelper
too, e.g., with GraphModelHelper::GetDirectPredecessors. An alternative and more manual approach would be using GraphNode.GetInnerNodes
and GraphNode.GetConnections
.
And as always, I am not really looking for a discussion about what you or I would consider a good API. I am telling you that you will be in a world of hurt when you terminate your errors everywhere as you did in your code. Your code will then just silently fail without you knowing why. So, I gave you an example on how to use our error handling.
Cheers,
Ferdinand
Hey @Dunhou ,
That is a good question, although slightly off-topic. What I used there is called trailing return type. It was introduced in C++11, specifically in the context of lambda expressions. The syntax allows you to specify the return type after the function parameters, which can be useful in certain situations, especially when dealing with complex types or when the return type depends on template parameters. It is functionally identical to the leading style. Python is deeply related to C and C++, and many of its conventions, concepts, and features are rooted in C/C++ programming practices.
I just went here instinctively for this notation, as it is somewhat common to use it when you talk about a function in terms of its signature, rather than its concrete implementation. It is not a requirement, and you can write the function both ways, we in fact - except for lambda expressions - do not use this notation in our codebase.
// This is the same as
Sum1(const int a, const int b) -> int { return a + b; }
// as this (except for the name so that they can coexist).
int Sum2(const int a, const int b) { return a + b; }
// But we need the trailing notation for lambdas, of which modern Maxon API Cinema 4D makes heavy use.
void Foo() {
auto add = [](const int a, const int b) -> int { return a + b; };
cout << add(1, 2) << std::endl;
}
So, to recap, we recommend to use Result<T>
as the return type for everything that comes into contact with Maxon API error handling, so that you can propagate errors. We do not make any strong recommendations regarding return type conventions. We slightly recommend the traditional leading return type notation, but it is not a requirement.
Cheers
Ferdinand
Hey @ECHekman,
The value of a port should be accessed with GraphNodeFunctions.GetPortValue or GraphNodeFunctions::GetEffectivePortValue
. Usually, the former is fine, and only for some specialty cases where the actual port data does not reflect what the user sees, and you want to access the user facing value, you have to use the latter.
What you are doing there, is the old access via GetValue
which is primarily meant for attribute access these days. But it should still work. As always for non-executable code examples, it is hard to judge what is going wrong there for you. But note that checks like these are not doing what you probably think they are doing:
ifnoerr(auto & typeNode = bumpNode.GetInputs().FindChild(maxon::Id("...")))
{
...
}
I personally would use auto
only sparingly and especially with the added line-break and the unfortunate placement of the reference operator had to look twice what you are doing here. But that is just personal taste, even if we wrote it like this in a more Maxonic way, it would not do what you probably think it does.
void SomeFunction()
{
iferr_scope_handler
{
// DiagnosticOutput("@ failed with error: @", MAXON_FUNCTIONNAME, err);
return;
};
const maxon::GraphNode& someInput = bumpNode.GetInputs().FindChild(maxon::Id("...")) iferr_return;
if (!someInput)
return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Input node not found"_s);
}
The return value of FindChild
will always be a maxon::GraphNode
, unless an internal error occurs. A non-valid node path will not raise an error but return an empty and with that invalid node.
if (someInput.IsValid())
{
// Do something with someInput
}
else
{
// Handle the case where the input node is not found
}
Cheers,
Ferdinand
edit: It is good to see that you are moving towards the Maxon API. But when I see things like this:
maxon::GraphNode bumpNode = getConnectedNode(bumpRes, maxon::NODE_KIND::NODE);
I am not so sure you are on the right track. When you write functions within the Maxon API, you should use our error handling. When you terminate the error handling within your functions, you will probably not have a good time. I.e., the signature should be this
getConnectedNode(const GraphNode& input, const NODE_KIND kind) -> Result<GraphNode>
and not -> GraphNode&
or -> GraphNode
. Note that there is also GraphModelHelperInterface
which likely already implements what you are implementing there. See the GraphModelInterface Manual for an overview.
I see your edited/deleted posting, did you find the mail? When push comes to shove, I can also just give you a bunch of plugin IDs.