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()
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
Hi,
you use GetActiveDocument()
in a NodeData
environment. You cannot do this, since nodes are also executed when their document is not the active document (while rendering for example - documents get cloned for rendering).
Cheers
zipit
Hi,
you have to invoke AddUserArea
and then attach an instance of your implemented type to it. Something like this:
my_user_area = MyUserAreaType()
self.AddUserArea(1000,*other_arguments)
self.AttachUserArea(my_user_area, 1000)
I have attached an example which does some things you are trying to do (rows of things, highlighting stuff, etc.). The gadget is meant to display a list of boolean values and the code is over five years old. I had a rather funny idea of what good Python should look like then and my attempts of documentation were also rather questionable. I just wrapped the gadget into a quick example dialog you could run as a script. I did not maintain the code, so there might be newer and better ways to do things now.
Also a warning: GUI stuff is usually a lot of work and very little reward IMHO.
Cheers
zipit
import c4d
import math
import random
from c4d import gui
# Pattern Gadget
IDC_SELECTLOOP_CELLSIZE = [32, 32]
IDC_SELECTLOOP_GADGET_MINW = 400
IDC_SELECTLOOP_GADGET_MINH = 32
class ExampleDialog(gui.GeDialog):
"""
"""
def CreateLayout(self):
"""
"""
self.Pattern = c4d.BaseContainer()
for i in range(10):
self.Pattern[i] = random.choice([True, False])
self.PatternSize = len(self.Pattern)
self.gadget = Patterngadget(host=self)
self.AddUserArea(1000, c4d.BFH_FIT, 400, 32)
self.AttachUserArea(self.gadget, 1000)
return True
class Patterngadget(gui.GeUserArea):
"""
A gui gadget to modify and display boolean patterns.
"""
def __init__(self, host):
"""
:param host: The hosting BaseToolData instance
"""
self.Host = host
self.BorderWidth = None
self.CellPerColumn = None
self.CellWidht = IDC_SELECTLOOP_CELLSIZE[0]
self.CellHeight = IDC_SELECTLOOP_CELLSIZE[1]
self.Columns = None
self.Height = None
self.Width = None
self.MinHeight = IDC_SELECTLOOP_GADGET_MINH
self.MinWidht = IDC_SELECTLOOP_GADGET_MINW
self.MouseX = None
self.MouseY = None
"""------------------------------------------------------------------------
Overridden methods
--------------------------------------------------------------------"""
def Init(self):
"""
Init the gadget.
:return : Bool
"""
self._get_colors()
return True
def GetMinSize(self):
"""
Resize the gadget
:return : int, int
"""
return int(self.MinWidht), int(self.MinHeight)
def Sized(self, w, h):
"""
Get the gadgets height and width
"""
self.Height, self.Width = int(h), int(w)
self._fit_gadget()
def Message(self, msg, result):
"""
Fetch and store mouse over events
:return : bool
"""
if msg.GetId() == c4d.BFM_GETCURSORINFO:
base = self.Local2Screen()
if base:
self.MouseX = msg.GetLong(c4d.BFM_DRAG_SCREENX) - base['x']
self.MouseY = msg.GetLong(c4d.BFM_DRAG_SCREENY) - base['y']
self.Redraw()
self.SetTimer(1000)
return gui.GeUserArea.Message(self, msg, result)
def InputEvent(self, msg):
"""
Fetch and store mouse clicks
:return : bool
"""
if not isinstance(msg, c4d.BaseContainer):
return True
if msg.GetLong(c4d.BFM_INPUT_DEVICE) == c4d.BFM_INPUT_MOUSE:
if msg.GetLong(c4d.BFM_INPUT_CHANNEL) == c4d.BFM_INPUT_MOUSELEFT:
base = self.Local2Global()
if base:
x = msg.GetLong(c4d.BFM_INPUT_X) - base['x']
y = msg.GetLong(c4d.BFM_INPUT_Y) - base['y']
pid = self._get_id(x, y)
if pid <= self.Host.PatternSize:
self.Host.Pattern[pid] = not self.Host.Pattern[pid]
self.Redraw()
return True
def Timer(self, msg):
"""
Timer loop to catch OnMouseExit
"""
base = self.Local2Global()
bc = c4d.BaseContainer()
res = gui.GetInputState(c4d.BFM_INPUT_MOUSE,
c4d.BFM_INPUT_MOUSELEFT, bc)
mx = bc.GetLong(c4d.BFM_INPUT_X) - base['x']
my = bc.GetLong(c4d.BFM_INPUT_Y) - base['y']
if res:
if not (mx >= 0 and mx <= self.Width and
my >= 0 and my <= self.Height):
self.SetTimer(0)
self.Redraw()
def DrawMsg(self, x1, y1, x2, y2, msg):
"""
Draws the gadget
"""
# double buffering
self.OffScreenOn(x1, y1, x2, y2)
# background & border
self.DrawSetPen(self.ColBackground)
self.DrawRectangle(x1, y1, x2, y2)
if self.BorderWidth:
self.DrawBorder(c4d.BORDER_THIN_IN, x1, y1,
self.BorderWidth + 2, y2 - 1)
# draw pattern
for pid, state in self.Host.Pattern:
x, y = self._get_rect(pid)
self._draw_cell(x, y, state, self._is_focus(x, y))
"""------------------------------------------------------------------------
Public methods
--------------------------------------------------------------------"""
def Update(self, cid=None):
"""
Update the gadget.
:param cid: A pattern id to toggle.
"""
if cid and cid < self.Host.PatternSize:
self.Host.Pattern[cid] = not self.Host.Pattern[cid]
self._fit_gadget()
self.Redraw()
"""------------------------------------------------------------------------
Private methods
--------------------------------------------------------------------"""
def _get_colors(self, force=False):
"""
Set the drawing colors.
:return : Bool
"""
self.ColScale = 1.0 / 255.0
if self.IsEnabled() or force:
self.ColBackground = self._get_color_vector(c4d.COLOR_BG)
self.ColCellActive = c4d.GetViewColor(
c4d.VIEWCOLOR_ACTIVEPOINT) * 0.9
self.ColCellFocus = self._get_color_vector(c4d.COLOR_BGFOCUS)
self.ColCellInactive = self._get_color_vector(c4d.COLOR_BGEDIT)
self.ColEdgeDark = self._get_color_vector(c4d.COLOR_EDGEDK)
self.ColEdgeLight = self._get_color_vector(c4d.COLOR_EDGELT)
else:
self.ColBackground = self._get_color_vector(c4d.COLOR_BG)
self.ColCellActive = self._get_color_vector(c4d.COLOR_BG)
self.ColCellFocus = self._get_color_vector(c4d.COLOR_BG)
self.ColCellInactive = self._get_color_vector(c4d.COLOR_BG)
self.ColEdgeDark = self._get_color_vector(c4d.COLOR_EDGEDK)
self.ColEdgeLight = self._get_color_vector(c4d.COLOR_EDGELT)
return True
def _get_cell_pen(self, state, _is_focus):
"""
Get the color for cell depending on its state.
:param state : The state
:param _is_focus : If the cell is hoovered.
:return : c4d.Vector()
"""
if state:
pen = self.ColCellActive
else:
pen = self.ColCellInactive
if self.IsEnabled() and _is_focus:
return (pen + c4d.Vector(2)) * 1/3
else:
return pen
def _draw_cell(self, x, y, state, _is_focus):
"""
Draws a gadget cell.
:param x: local x
:param y: local y
:param state: On/Off
:param _is_focus: MouseOver state
"""
# left and top bright border
self.DrawSetPen(self.ColEdgeLight)
self.DrawLine(x, y, x + self.CellWidht, y)
self.DrawLine(x, y, x, y + self.CellHeight)
# bottom and right dark border
self.DrawSetPen(self.ColEdgeDark)
self.DrawLine(x, y + self.CellHeight - 1, x +
self.CellWidht - 1, y + self.CellHeight - 1)
self.DrawLine(x + self.CellWidht - 1, y, x +
self.CellWidht - 1, y + self.CellHeight - 1)
# cell content
self.DrawSetPen(self._get_cell_pen(state, _is_focus))
self.DrawRectangle(x + 1, y + 1, x + self.CellWidht -
2, y + self.CellHeight - 2)
def _get_rect(self, pid, offset=1):
"""
Get the drawing rect for an array id.
:param pid : the pattern id
:param offset : the pixel border offset
:return : int, int
"""
pid = int(pid)
col = pid / self.CellPerColumn
head = pid % self.CellPerColumn
return self.CellWidht * head + offset, self.CellHeight * col + offset
def _get_id(self, x, y):
"""
Get the array id for a coord within the gadget.
:param x : local x
:param y : local y
:return : int
"""
col = (y - 1) / self.CellHeight
head = (x - 1) / self.CellWidht
return col * self.CellPerColumn + head
def _is_focus(self, x, y):
"""
Test if the cell coords are under the cursor.
:param x : local x
:param y : local y
:return : bool
"""
if (self.MouseX >= x and self.MouseX <= x + self.CellWidht and
self.MouseY >= y and self.MouseY <= y + self.CellHeight):
self.MouseX = c4d.NOTOK
self.MouseY = c4d.NOTOK
return True
else:
return False
def _fit_gadget(self):
"""
Fit the gadget size to the the array
"""
oldHeight = self.MinHeight
self.CellPerColumn = int((self.Width - 2) / self.CellWidht)
self.Columns = math.ceil(
self.Host.PatternSize / self.CellPerColumn) + 1
self.MinHeight = int(IDC_SELECTLOOP_GADGET_MINH * self.Columns) + 3
self.MinWidht = int(IDC_SELECTLOOP_GADGET_MINW)
self.BorderWidth = self.CellWidht * self.CellPerColumn
if oldHeight != self.MinHeight:
self.LayoutChanged()
def _get_color_vector(self, cid):
"""
Get a color vector from a color ID.
:param cid : The color ID
:return : c4d.Vector()
"""
dic = self.GetColorRGB(cid)
if dic:
return c4d.Vector(float(dic['r']) * self.ColScale,
float(dic['g']) * self.ColScale,
float(dic['b']) * self.ColScale)
else:
return c4d.Vector()
if __name__ == "__main__":
dlg = ExampleDialog()
dlg.Open(c4d.DLG_TYPE_ASYNC, defaultw=400, defaulth=400)
@mogh said in How to receive the high definition / Interpolation version of a spline?:
from the above helper I only get the points the spline is set to (eg. adaptive 5°) but I perhaps want more points in between.
That is not correct. As I said, splines have theoretically infinite precision, you are only bound in practice by the floating point precision you use for your computations - double, i.e., 64 bit in Cinema 4D. You get in your code just the cache of your spline and iterate over its points. That is more or less the same as if you would have just called BaseObject.GetCache()
on your SplineObject
which I mentioned above.
When you want to interpolate your spline manually, you must use the interpolation methods of SplineHelp
, e.g., GetMatrix
, which is sort of the most potent of all, as it gives you a full frame for each sample (i.e., not just a point but also an orientation).
I have a hunch that the actual question is how to build the cache of a spline as if it would have higher interpolation settings. And for that you should copy your input (unless it is already a dangling, i.e., non-inserted node), change its settings, insert it into a dummy document, and then call ExecutePasses
on that document. Then you can get the cache, and iterate over its points.
Cheers,
Ferdinand
import c4d
doc: c4d.documents.BaseDocument # The currently active document.
op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`.
def main() -> None:
"""Called by Cinema 4D when the script is being executed.
"""
if not op:
return
helper = c4d.utils.SplineHelp()
if not helper.InitSplineWith(op, c4d.SPLINEHELPFLAGS_RETAINLINEOBJECT):
raise RuntimeError("Could not initialize spline helper.")
# Take exactly ten samples over the length of the spline, no matter how many vertices it has,
# and how many points its current LineObject cache has.
steps: int = 10
for i in range(steps + 1):
# Get a frame for the offset #t.
t: float = i/float(steps)
m: c4d.Matrix = helper.GetMatrix(t)
# Print the #t, the point #p on the spline, its normal #n, tangent #t, and bi-tangent #bt.
print(f"{t = }, p = {m.off}, n = {m.v3}, t = {m.v1}, bt = {m.v2}")
if __name__ == '__main__':
main()
Hey @mogh,
that is not possible, nodes, i.e., NodeData
, e.g., TagData
, always need a description (and with that res file).
Cheers,
Ferdinand
Hey @mogh,
Thank you for reaching out to us. What do you mean with "low" detail? Something like the sampling angle of a spline in "adaptive" mode? The interpolation settings of a spline only apply when you quantize it, i.e., the degree of precision with which a SplineObject
is being quantized into its LineObject
cache (i.e., what is returned for BaseObject.GetCache()
for a spline).
Splines themselves are just series of polynomials and therefore have no inherent precision. So, when you call for example SplineHelp.GetMatrix()
to get all the metadata for an offset, you are not subject to the interpolation settings. That is within the limits of what computers can do regarding arithmetic of irrational numbers. You can quite quickly get subject to floating point precision problems when working with splines. If they are fixable and how to fix them, depends on the concrete case.
Cheers,
Ferdinand
When this is what you want, a Python Programming Tag has also a draw function, i.e., the scripting element inside Cinema 4D. Scripting elements make great prototypes for their full grown plugin counter part (for the Python tag that would be TagData
) but can also be very potent on their own while providing phenomenally rapid development speed.
Hey,
A tag can draw into a viewport My line of thinking was that you want some form of automatism/ streamlined product. E.g., an "enable/disable comb drawing" button (CommandData) and then a node (TagData/ObjectData) actually implementing it. When you just want to have the ability to manually enhance a spline with some drawing decorations, a tag would be a very good choice.
Cheers,
Ferdinand
Hello @DayDreamer,
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: Asking Questions.
This is your first posting and you did well, but I would point you to our 'singular question' rule, as lined out above in the 'Asking Questions' link. While we usually cut user some slack regrading that, the rule exists for a reason, as topics otherwise tend to become gigantic. There are way too many questions in this topic, please split your questions into multiple topics in the future.
Cinema 4D | Extensions > Tools > Source Code Protector
which will encrypt a pyp
Python module. But code must of course be decrypted at runtime to be sent to the Python VM, so, this will not really stop anyone who really wants your source code (a cracker for example). C++ is the by far safer option when intellectual property is important (but also C++ can be decompiled).Cheers,
Ferdinand
Thanks for helping out @spedler!
Hey @mogh,
Thank you for reaching out to us. The answer depends a bit on the context, and I could write here an essay but I will try to keep it short. Feel free to ask follow-up questions.
In principle, there are two meaningful ways to do this:
ToolData
hook in Python. The issue with this is that this would be then a tool, i.e., you could not have it enabled in parallel to the move tool. The advantage is that this is exposed in Python and relatively little work.SceneHookData
is not wrapped for Python. In our morning meeting we decided that we will change that in an upcoming version of the Python API, but it is not yet decided if we will include drawing for scene hooks, as the non-implementation of SceneHookData
was born out of performance concerns (which are not very practical IMHO).A CommandData
and GeDialog
combo will not work, because while you can retrieve the BaseDraw
instances of a document at any time, you need the context of a drawing method for your drawing instructions to be actually registered (otherwise the viewport will just ignore your calls). And neither command nor dialogs have these methods.
In Python you can sort of emulate a scene hook with an MessageData
and ObjectData
combo, the message hook scans for new scenes being opened, and then inserts the object as a hidden node. The object could then do the drawing. Via MSG_DOCUMENTINFO
the object could then remove itself shortly before the document is being saved. You could also do something similar with a CommandData
and TagData
combo, i.e., a toggle command which enables or disables "comb drawing" on an object by placing a hidden tag on it.
What to chose depends on your project details. Scene hooks are the cleanest option. When a tool somehow makes sense, I would go for that. The MessageData
+ ObjectData
combo is very messy, but could technically draw on all objects at once. TheCommandData
option would be more meant for drawing on objects one by one. But both of these latter options are not ideal and will require an experienced developer to make them smooth.
Cheers,
Ferdinand
Hey @Havremunken,
I have posted the code example which will be shipped with an upcoming C++ SDK here. The example does not do 100% what you are trying to do, the node type implementing the custom branch is for example an object and not a scene hook, as I had to keep common applicability a bit in mind.
But it realizes a node which inernalizes other scene elements in a custom branch of itself, and should get you at least going. When you still need help with your specific case, please ask questions in this thread. When you have generic branching questions tied to the example, please use the other thread.
Cheers,
Ferdinand
Dear Community,
Custom branching, i.e., adding custom data to a Cinema 4D scene graph, was always possible in the C++ API, but never clearly documented. I therefore added a new code example to our C++ SDK which demonstrates such custom branching. The example will be shipped with an upcoming SDK, but you can find a preview below.
Cheers,
Ferdinand
example.main.zip: Contains the changed parts of the example.main SDK project, i.e., the new objectdata_assetcontainer
example.
This is just the main code, for all the code and the resources, see the files.
/* Realizes a generator object acting as an "asset container" which can internalize other objects
- assets - into itself to be later retrieved and to be stored with a scene file.
This demonstrates how to implement storing scene elements with a custom purpose in a custom
dedicated part of the scene graph of a Cinema 4D document. As natively implemented for objects,
tags, materials, shaders, and many more things - which all have all have a custom purpose and
dedicated place in a Cinema 4D document.
Usage:
Open the Commander with "Shift + C" and type "Asset Container" to find and then add an Asset
Container object. Now add primitives like a cube and a sphere to the scene. Drag them into the
"Source" link of the Asset Container and press then "Add" to internalize them into the custom
branch of the container. You can now delete the originals, and display the internal assets with
the "Selection" parameter as the output of the node. When load or store an Asset Container with
a scene or copy one Asset Container to another, its internal branching data will persist.
See also:
Technical Overview:
Branching is one of the fundamental principles with which Cinema 4D realizes a scene graph.
Branches are a way to associate a scene element (#GeListNode) with another scene element other
than pure hierarchical relations also realized by #GeListNode (GetUp, GetDown, GetNext, etc.).
A Cinema 4D document (a #BaseDocument) is for example also a type of #GeListNode itself and has
an #Obase branch for the objects stored in itself and an #Mbase branch for the materials stored
itself. Inside the #Obase branch, objects are just linked hierarchically, just as users see them
in the Object Manger. But each #BaseObject (which is also of type #GeListNode) also has branches,
for example a Tbase branch which holds the tags of that object. With that #GeListNode realizes
via branching a scene graph as a set of trees which are linked via branching relations.
Some node tree Another node tree
Node.0 --- Foo branch of Node.0 ---> GeListHead of Foo Branch
Node.0.1 Node.A
Node.1 Node.B
Node.1.1 Node.B.A
...
GeListNode::GetBranchInfo exposes the branches of a scene element. Plugins can implement custom
branches by implementing NodeData::GetBranchInfo which feeds the former method. This code example
demonstrates implementing such custom branching relation.
See also:
gui/activeobject.cpp: A plugin which visualizes the branches in a Cinema 4D document.
*/
// Author: Ferdinand Hoppe
// Date: 14/02/2025
// Copyright: Maxon Computer
#include "c4d_baseobject.h"
#include "c4d_basedocument.h"
#include "c4d_general.h"
#include "c4d_objectdata.h"
#include "lib_description.h"
#include "oassetcontainer.h"
// Plugin ID of the #AssetContainerObjectData plugin interface. You must register your plugin IDs
// via developers.maxon.net.
static const cinema::Int32 Oassetcontainer = 1065023;
using namespace cinema;
using namespace maxon;
/// @brief Realizes a type which can internalize other nodes in a custom branch of itself.
///
/// @details These other nodes could be all of a specific (custom) type, or has shown here, of a
/// builtin base type such as a branch which can hold objects. When we want to store custom data
/// per node in our scene graph, e.g., some meta data for assets, we have two options:
///
/// 1. Just store the information in the container of a native node. We can register a plugin ID
/// and write a BaseContainer under that plugin ID into the data container of the native node.
/// And while this is technically supported, writing alien data into foreign containers is
/// never a good thing as it always bears the risk of ID collisions (always check before
/// writing alien data if the slot is empty or just occupied by a BaseContainer with your
/// plugin ID).
/// 2. Implement a custom node, so that we can customize its ::Read, ::Write, and ::CopyTo
/// functions to store our custom data.
///
/// The common pattern to join (2) with builtin data is using tags. So, let's assume we want to
/// store objects in a custom branch like we do here, but also decorate each asset with meta data.
/// We then implement a tag, where we (a) can freely write into the data container, and (b) also
/// manually serialize everything that does not easily fit into a BaseContainer. Each asset in the
/// branch would then be decorated with such tag. For re-insertion the tag could then either be
/// removed, or we could also have these tags persist in the Object Manager but hide them from the
/// user (Cinema 4D itself uses a lot of hidden tags to store data).
// ------------------------------------------------------------------------------------------------
class AssetContainerObjectData : public ObjectData
{
INSTANCEOF(AssetContainerObjectData, ObjectData)
private:
/// @brief The branch head of the custom asset data branch held by each instance of an
/// #Oassetcontainer scene element.
// ----------------------------------------------------------------------------------------------
AutoAlloc<GeListHead> _assetHead;
public:
/// @brief Allocator for this plugin hook.
// ----------------------------------------------------------------------------------------------
static NodeData* Alloc()
{
return NewObjClear(AssetContainerObjectData);
}
/// @brief Called by Cinema 4D to let the node initialize its values.
// ----------------------------------------------------------------------------------------------
Bool Init(GeListNode* node, Bool isCloneInit)
{
// Auto allocation of _assetHead has failed, this probably means we are out of memory or
// otherwise royally screwed. One of the rare cases where it makes sense to return false.
if (!_assetHead)
return false;
// Attach our branch head to its #node (#node is the #BaseObject representing this
// #AssetContainerObjectData instance).
_assetHead->SetParent(node);
return true;
}
// --- IO and Branching -------------------------------------------------------------------------
// One must always implement NodeData::Read, ::Write, and ::CopyTo together, even when one only
// needs one of them. We implement here our custom branch being read, written, copied, and accessed
// in scene traversal.
/// @brief Called by Cinema 4D when it deserializes #node in a document that is being loaded
/// to let the plugin do custom deserialization for data stored in #hf.
// ----------------------------------------------------------------------------------------------
Bool Read(GeListNode* node, HyperFile* hf, Int32 level)
{
// Call the base implementation, more formality than necessity in our case.
SUPER::Read(node, hf, level);
// Read our custom branch back. "Object" means here C4DAtom, i.e., any form of scene element,
// including GeListNode and its derived types.
if (!_assetHead->ReadObject(hf, true))
return false;
return true;
}
/// @brief Called by Cinema 4D when it serializes #node in a document that is being saved
/// to let the plugin do custom serialization of data into #hf.
// ----------------------------------------------------------------------------------------------
Bool Write(const GeListNode* node, HyperFile* hf) const
{
// Call the base implementation, more formality than necessity in our case.
SUPER::Write(node, hf);
// Write our custom branch into the file. "Object" is here also meant in an abstract manner.
if (!_assetHead->WriteObject(hf))
return false;
return true;
}
/// @brief Called by Cinema 4D when a node is being copied.
// ----------------------------------------------------------------------------------------------
Bool CopyTo(
NodeData* dest, const GeListNode* snode, GeListNode* dnode, COPYFLAGS flags, AliasTrans* trn) const
{
// The #AssetContainerObjectData instance of the destination node #dnode to which we are going
// to copy from #this.
AssetContainerObjectData* other = static_cast<AssetContainerObjectData*>(dest);
if (!other)
return false;
// Copy the branching data from #this to the #other.
if (!this->_assetHead->CopyTo(other->_assetHead, flags, trn))
return false;
return SUPER::CopyTo(dest, snode, dnode, flags, trn);
}
/// @brief Called by Cinema 4D to get custom branching information for this node.
///
/// @details This effectively makes our custom branch visible for every one else and with that
/// part of the scene graph.
// ----------------------------------------------------------------------------------------------
Result<Bool> GetBranchInfo(const GeListNode* node,
const ValueReceiver<const BranchInfo&>& info, GETBRANCHINFO flags) const
{
iferr_scope;
yield_scope;
NodeData::GetBranchInfo(node, info, flags) yield_return;
// Return branch information when the query allows for empty branches or when query is
// only for branches which do hold content and we do hold content.
if (!(flags & GETBRANCHINFO::ONLYWITHCHILDREN) || _assetHead->GetFirst())
{
// We return a new #BranchInfo wrapped in the passed value receiver, where we express
// information about our custom branch. Since #info is a value receiver, we could invoke
// it multiple times in a row to express multiple branches we create.
info(
BranchInfo {
// The head of our custom branch. MAXON_REMOVE_CONST is required as #this is const in
// this context due to this being a const method.
MAXON_REMOVE_CONST(this)->_assetHead,
// A human readble label/identifier for the branch. Cinema 4D does not care how we name
// our branch and if we choose the same name as other branches do. But making your branch
// name sufficiently unique is still recommend, for example by using the reverse domain
// notation syntax.
"net.maxonexample.branch.assetcontainer"_s,
// The identifier of this branch. This technically also does not have to be unique. Here
// often an ID is chosen which reflects the content to be found in the branch. E.g., Obase
// for branches that just hold objects. We choose here the ID of our node type.
Oassetcontainer,
// The branch flags, see docs for details.
BRANCHINFOFLAGS::NONE }) yield_return;
}
return true;
}
// --- End of IO --------------------------------------------------------------------------------
/// @brief Called by Cinema 4D to let this generator contstruct its geometry output.
// ----------------------------------------------------------------------------------------------
BaseObject* GetVirtualObjects(BaseObject* op, const HierarchyHelp* hh) {
// Cinema 4D calls all generators upon each scene update, no matter if any of the inputs for
// the generator has changed. It is up to the generator to decide if a new cache must be
// constructed. While this technically can be ignored, it will result in a very poor performance
// to always fully reconstruct one's caches from scratch.
// We consider ourselves as dirty, i.e., as requiring a recalculation of our ouput, when
// there is no cache yet or when one of our parameter values recently changed (DIRTYFLAGS::DATA).
// When we are not dirty, we just return our previous result, the cache of the object (#op
// is the #BaseObject representing #this #AssetContainerObjectData instance).
const Bool isDirty = op->CheckCache(hh) || op->IsDirty(DIRTYFLAGS::DATA);
if (!isDirty)
return op->GetCache();
// Build a new cache based on the asset the user selected in the "Selection" drop-down.
const BaseContainer* const data = op->GetDataInstance();
const Int32 selectionIndex = data->GetInt32(ASSETCONTAINER_SELECTION, NOTOK);
// When paramater access has failed (should not happen) or the user has selected 'None', we
// return a Null object as our new cache.
if (selectionIndex == NOTOK) // Should not happen.
return BaseObject::Alloc(Onull);
// Otherwise decrement the index by one (to compensate for 'None') and get a copy of the
// matching asset and return it (unless something went wrong, then again return a Null).
BaseObject* const asset = GetAssetCopy(selectionIndex - 1);
if (!asset)
return BaseObject::Alloc(Onull);
return asset ? asset : BaseObject::Alloc(Onull);
}
/// @brief Called by Cinema 4D to convey messages to a node.
///
/// @details We use it here to react to a click on our "Add" button.
// ----------------------------------------------------------------------------------------------
Bool Message(GeListNode *node, Int32 type, void *data)
{
switch (type)
{
// The user pressed some button in the UI of #node ...
case MSG_DESCRIPTION_COMMAND:
{
DescriptionCommand* const cmd = reinterpret_cast<DescriptionCommand*>(data);
if (!cmd)
return SUPER::Message(node, type, data);
const Int32 cmdId = cmd->_descId[0].id;
switch (cmdId)
{
// ... and it was our "Add" button.
case ASSETCONTAINER_ADD:
{
// Get the data container of #node and retrieve the object linked as the new asset.
BaseObject* const obj = static_cast<BaseObject*>(node);
if (!obj)
return true;
const BaseContainer* const bc = obj->GetDataInstance();
const BaseObject* const link = static_cast<
const BaseObject*>(bc->GetLink(ASSETCONTAINER_SOURCE, obj->GetDocument()));
if (!link)
return true;
// Attempt to add #link as an asset into the Oassetcontainer branch of #obj. Pressing
// a button and modifying our internal branch does not make #obj dirty on its own, we
// therefore flag ourself, so that our GUI is being rebuild (and also our cache).
if (AddAsset(link))
{
StatusSetText(FormatString("Added '@' as an asset.", link->GetName()));
obj->SetDirty(DIRTYFLAGS::DATA);
}
else
{
StatusSetText(FormatString("Could not add asset '@'.", link->GetName()));
}
break;
}
}
break;
}
}
return SUPER::Message(node, type, data);
}
/// @brief Called by Cinema 4D to let a node dynamically modify its description.
///
/// @details The .res, .str, and .h files of a node define its description, its GUI and language
/// strings. We use the method here to dynamically populate the content of the "Selection"
/// dropdown with the names of the nodes found below #_assetHead.
// ----------------------------------------------------------------------------------------------
Bool GetDDescription(const GeListNode* node, Description* description, DESCFLAGS_DESC& flags) const
{
// Get out when the description for #node->GetType() (i.e., Oassetcontainer) cannot be loaded.
// Doing this is important and not an 'this can never happen' precaution.
if (!description->LoadDescription(node->GetType()))
return false;
// The parameter Cinema 4D is currently querying to be updated and the parameter we want to
// update, our "Selection" drop-down. When Cinema does not give us the empty/null query and the
// query is not for "Selection", then get out to not update the description over and over again.
const DescID* queryId = description->GetSingleDescID();
const DescID paramId = ConstDescID(DescLevel(ASSETCONTAINER_SELECTION));
if (queryId && !paramId.IsPartOf(*queryId, nullptr))
return SUPER::GetDDescription(node, description, flags);
// Get the description container for the "Selection" parameter and start modifying it.
AutoAlloc<AtomArray> _;
BaseContainer* bc = description->GetParameterI(paramId, _);
if (bc)
{
// Create a container which holds entries for all assets which will be our drop-down item
// strings, following the form { 0: "None, 1: "Cube", 2: "Sphere, ... }.
BaseContainer items;
items.SetString(0, "None"_s);
BaseArray<const BaseObject*> assets;
if (!GetAssetPointers(assets))
return SUPER::GetDDescription(node, description, flags);
Int32 i = 1;
for (const BaseObject* const item : assets)
items.SetString(i++, item->GetName());
// Set this container as the cycle, i.e., drop-down, items of #ASSETCONTAINER_SELECTION and
// signal that we modified the description by appending #DESCFLAGS_DESC::LOADED.
bc->SetContainer(DESC_CYCLE, items);
flags |= DESCFLAGS_DESC::LOADED;
}
return SUPER::GetDDescription(node, description, flags);
}
/// --- Custom Methods (not part of NodeData/ObjectData) ----------------------------------------
/// @brief Returns a list of pointers to the internalized assets.
///
/// @param[out] assets The asset pointers to retrieve.
/// @return If the operation has been successful.
// ----------------------------------------------------------------------------------------------
bool GetAssetPointers(BaseArray<const BaseObject*>& assets) const
{
iferr_scope_handler
{
return false;
};
if (!_assetHead)
return false;
const BaseObject* asset = static_cast<const BaseObject*>(_assetHead->GetFirst());
while (asset)
{
assets.Append(asset) iferr_return;
asset = asset->GetNext();
}
return true;
}
/// @brief Returns a copy of the asset at the given #index.
///
/// @details We implement this so that our cache does not use the same data as stored in the
/// Oassetcontainer branch, as ObjectData::GetVirtualObjects should always return an object
/// tree that is not already part of a document.
///
/// @param[in] index The index of the asset to copy.
/// @return The copied asset or the nullptr on error.
// ----------------------------------------------------------------------------------------------
BaseObject* GetAssetCopy(const Int32 index)
{
BaseObject* asset = _assetHead ? static_cast<BaseObject*>(_assetHead->GetFirst()) : nullptr;
if (!asset)
return nullptr;
Int32 i = 0;
while (asset)
{
// It is important that we copy the node without its bit flags, as we otherwise run into
// troubles with cache building.
if (i++ == index)
return static_cast<BaseObject*>(asset->GetClone(COPYFLAGS::NO_BITS, nullptr));
asset = asset->GetNext();
}
return nullptr;
}
/// @brief Copies the given #asset and adds it to the internal asset data branch.
///
/// @details In a production environment, we should be more precise in what we copy and what not.
/// We copy here without any hierarchy but with branches. Branches of an object would be for
/// example its tags and tracks. Copying such nested data can never really cause severe technical
/// issue, but we could for example internalize a material tag like this which interferes with our
/// rendering. But when we set here COPYFLAGS::NO_BRANCHES, or alternatively just blindly remove
/// all tags from an object, we would also remove the hidden point and polygon data tags of an
/// editable polygon object and with that all its content. So, internalizing "assets" in such
/// manner will require good book-keeping in practice of what is needed and what not.
///
/// @param[in] asset The asset to add.
/// @return If the operation has been successful.
// ----------------------------------------------------------------------------------------------
bool AddAsset(const BaseObject* asset)
{
if (!_assetHead)
return false;
// Here we should ensure that we do not internalize the same asset twice, for example via
// GeMarker. I did not do this, as this would have required more book keeping and I wanted to
// keep the example compact.
// Copy the node (making sure to keep its marker via #PRIVATE_IDENTMARKER) and insert the copy.
BaseObject* const copy = static_cast<BaseObject*>(
asset->GetClone(COPYFLAGS::NO_HIERARCHY | COPYFLAGS::NO_BITS, nullptr));
if (!copy)
return false;
_assetHead->InsertLast(copy);
return true;
}
};
/// @brief Called by the main.cpp of this module to register the #Oassetcontainer plugin when
/// Cinema 4D is starting.
// ------------------------------------------------------------------------------------------------
Bool RegisterAssetContainerObjectData()
{
// You must use here a unqiue pluin ID such as #Oassetcontainer.
return RegisterObjectPlugin(Oassetcontainer, "Asset Container"_s, OBJECT_GENERATOR,
AssetContainerObjectData::Alloc, "oassetcontainer"_s, nullptr, 0);
}