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
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)
Hi,
a bilinear interpolation is quite straight forward. If you have the quadrilateral Q with the the points,
c---d
| |
a---b
then the bilinear interpolation is just,
ab = lerp(a, b, t0)
cd = lerp(c, d, t0)
res = lerp(ab, cd, t1)
where t0, t1
are the interpolation offset(s), i.e. the texture coordinates in your case (the ordering/orientation of the quad is obviously not set in stone). I am not quite sure what you do when rendering normals, but when you render a color gradient, in a value noise for example, you actually want to avoid linear interpolation, because it will give you these ugly star-patterns. So you might need something like a bi-quadratic, bi-cubic or bi-cosine interpolation, i.e. pre-interpolate your interpolation offsets.
If I am not overlooking something, this should also work for triangles when you treat them as quasi-quadrilaterals like Cinema does in its polygon type.
Cheers,
zipit
Hello @ezeuz,
Thank you for reaching out to us. I can reproduce this, and what is even more odd, I can reproduce this back to R25, where it for sure once worked on my machine.
I am quite frankly a bit perplexed by what is going wrong here. This is the code which is failing here:
//Check if a pyp file is currently loaded. Used to check if a user registers a plugin from the pluginmanager. Requires GIL, sets the exception text.
static Bool CheckIfLoadedFromPypFile()
{
if (!current_plugin)
{
CPyErr_Format(PyExc_EnvironmentError, "cannot find pyp file - plugin registration failed");
return false;
}
return true;
}
Since this is failing so far back for me, there are two options:
c4dpy
manual and I tested there each command I documented.current_plugin
is null. This sounds even more unlikely.At the end of the day, this is more @m_adam's domain, as he owns c4dpy
. Let me sleep a night over this and also try the same on macOS (or have a look with a debugger when I find the time). When I then cannot find a solution, I will flag this thread as a bug, and Maxime will have to take a look.
I am pretty sure we are overlooking something here ...
Cheers,
Ferdinand
edit: There could be a bug in recent versions of Cinema 4D which does not unload binaries correctly. Try rebooting, I currently cannot do this as I am compiling, will try on Monday.
Hello @j_vogel,
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.
As lined out above, please follow our support procedures. Thread necromancy usually leads to problems, at least when that much time has passed, I have forked this thread. Please also follow our rules about Support Procedures: Asking Questions, in short, we do not answer "please implement this or that for me".
The parameter DOCUMENT_DOCUNIT
in a document is just something that is added to things like the coordinate manager, to set the unit and a multiplier for the numbers shown there. It will not scale the actual geometries (as it otherwise would be destructive) and therefore does not affect the view port.
To scale all elements in the scene, you must scale all elements . You can use the command which we implemented for that, what is talked about above and which can be invoked with the button in the document scale settings. But you then have to use its UI, which is I guess not what you want. So, you would have write something like that yourself. But there is currently no premade scene traversal in Python which can be a bit daunting for beginners (will change with the next major release). Carrying out the scaling itself is not that hard, although this complexity can also vary based on the content. Since I know you have beta access: You can also pick the latest beta build, the traversal stuff is already in there.
Cheers,
Ferdinand
"""Demonstrates how to scale all objects in a scene by a given factor.
"""
import c4d
SCALE_FACTOR: c4d.Vector = c4d.Vector(2) # Scale factor for the objects, we pick a uniform scale of 2.
doc: c4d.documents.BaseDocument # The currently active document.
def IterateTree(node: c4d.GeListNode, yieldSiblings: bool = False) -> c4d.GeListNode:
"""Iterates over all descendants of the given #node and optionally its next siblings.
In the next major release, there will be (finally) built-in scene traversal functions in Python. The one
to pick for this case would be `mxutils.IterateTree`. Its signature is compatible with what I did here.
"""
def iterate(node: c4d.GeListNode) -> c4d.GeListNode:
if not node:
return
yield node
for child in node.GetChildren():
yield from iterate(child)
while node:
yield from iterate(node)
node = node.GetNext() if yieldSiblings else None
def main() -> None:
"""Called by Cinema 4D when the script is being executed.
"""
# Iterate over all objects in the document and scale them.
for node in IterateTree(doc.GetFirstObject(), yieldSiblings=True):
node.SetMl(node.GetMl() * c4d.utils.MatrixScale(SCALE_FACTOR))
c4d.EventAdd()
if __name__ == '__main__':
main()
edit: you should of course use the local and not global matrix, as you otherwise will scale things multiple times.
Hello @shir,
Thank you for reaching out to us. It should be self-explanatory that we cannot say much about 'the maxon site is broken sending me from broken link to broken link'. What is 'the' maxon site for you? And there are not countless broken links on our sites, that all sounds very hyperbolic.
The old site which also hosts the plugin IDs is still operational: https://plugincafe.maxon.net/, you can also access it as embedded content via https://developers.maxon.net/forum/pid/, but you will have to login with your old plugin cafe account in both cases or create a new one.
Cheers,
Ferdinand
Hey @lasselauch,
We are not able to reproduce this crash on an Intel, M1, or M3 MacBook with 2024.4.0
. Please provide and submit a crash report when this is still a problem for you. I would also recommend reinstalling Cinema 4D to rule out that your installation was damaged.
Cheers,
Ferdinand
Hey @lasselauch,
we will have a look at the crash, I will move the thread here into bugs when we can confirm it.
About your problem at hand: I have written a code example which should get you going. In short, the message data is stored under BFM_CORE_PAR1
for p1
and _PAR2
for p2
. For the rest, please read the example. But I personally would still advise against taking such route unless one really has to.
Make sure to set USE_CTYPES
to FALSE
in the script when you still encounter issues importing it.
Cheers,
Ferdinand
"""Demonstrates sending messages using both SpecialEventAdd() and a custom message stream
implementation.
The general idea of the MessageStream type is to have centralized place to store data as key-value
pairs. In most cases, we can just define and expose such data container in a module which is imported
by all plugins which have to share the data. I went here the slightly more complex route of the case
where plugins are not able to do this, and all come with their own implementation and the data then
being share via a "standard" module like sys, os, or what I picked here, c4d.
This also demonstrates the most simple use case of SpeicalEventAdd(), CoreMessage() and ctypes,
where we send an integer as the message data. As expressed in the thread above, I am not a big fan
of this approach, this is all very un-pythonic and error prone.
We technically COULD do all what MessageStream does and more here with ctypes alone, but the more
complex our data gets, the more we will have to do to unpack our data from the wrapping capsule, and
when we want to send anything else than ints, we will also have to start packing up data when
sending them. This is all just a huge pain in the *** unless you are someone who is very comfortable
with the Python C API and the memory layout of your sent data anyway.
Note:
What I have not done here, is made this thread safe. If we have multiple threads sending messages
to the message stream, we will have to make sure that we do not run into access conflicts. This
could either be done via calling c4d.threading.GeThreadLock() before accessing the message stream
and .GeThreadUnlock() after, but that will really lock up everything which is not so great. An
alternative would be to use a Python's threading.Lock(), threading.RLock() or threading.Semaphore()
to lock the access to a message stream. The problem is there that our threads, c4d.threading.
C4DThread, are not fully compatible with Python's threading, so be careful when using them
together. But I have used threading.Semaphore() in the past without encountering any issues.
"""
USE_CTYPES: bool = True # Toggle for using ctypes and with it SpecialEventAdd() or not. Disable this
# when you experience crashes when importing ctypes.
IS_DEBUG: bool = True # Toggle for debug behavior.
PID_MY_CORE_MESSAGE: int = 1063958 # A registered plugin ID to uniquely identify our core message.
import c4d
import time
import inspect
if USE_CTYPES:
import ctypes
from mxutils import CheckType
class MessageStream:
"""Realizes a message stream that can be observed by others.
An observer is just a delegate, i.e., a function that is called when 'something' happens in the
message stream. Or in other words, more or less what the classic API message functions do in
Cinema 4D. The main difference between the observer/delegate pattern and old school messages is
that one has to manually register with the observable, here via AddObserver().
Can also be used as a plain message list without any observers, i.e., in conjunction with
SpecialEventAdd() and CoreMessage().
I used here ints as message IDs, but we could also use strings or any other hashable type. I
have done this solely so that the type works better in conjunction with SpecialEventAdd() which
is int focused.
See also:
https://en.wikipedia.org/wiki/Observer_pattern
Example:
# Define a function that will be called when something happens in the message stream,
# the observable in Maxon terms, we are interested in.
def OnMessageStreamEvent(event: str, mid: int, data: any) -> bool:
if event == MessageStream.EVENT_ADD:
print (f"An item with ID {mid} was added to the message stream.")
return True # Consume the event.
return False # Otherwise do not consume the event.
# Get the message stream with the handle 'main' and add our observer to it.
stream: MessageStream = MessageStream.GetStream("main")
stream.AddObserver(OnMessageStreamEvent)
# Somewhere else and some time later we can now send messages to the stream.
MessageStream.GetStream("main").Put(1, "Hello world!")
# Will print:
# An item with ID 1 was added to the message stream.
"""
EVENT_ADD: str = "ADD" # And item was added to the message stream.
EVENT_REMOVE: str = "REMOVE" # An item was removed from the message stream.
EVENT_FLUSH: str = "FLUSH" # The message stream was flushed.
EVENT_UPDATE: str = "UPDATE" # An item in the message stream was updated.
ATTACHMENT_POINT: object = c4d # The object/module to attach the global message streams to.
STREAM_ATTRIBUTE: str = "__MESSAGE_STREAM_" # The prefix for the global message stream attributes.
# --- Object model -----------------------------------------------------------------------------
def __init__(self, handle: str = ""):
"""Initializes the message stream with an optional handle.
"""
self._handle: str = CheckType(handle, str)
self._data: dict[int, any] = {}
self._observers: list[callable] = []
def __str__(self) -> str:
"""Returns a string representation of the message stream.
"""
return (f"<MessageStream('{self._handle}') at {hex(id(self)).upper()} with "
f"{len(self._observers)} observers>")
def __repr__(self) -> str:
"""Returns a string representation of the message stream.
"""
return str(self)
def __len__(self) -> int:
"""Returns the number of messages in the message stream.
"""
return len(self._data)
def __contains__(self, mid: int) -> bool:
"""Checks if a message ID is in the message stream.
"""
return mid in self._data
def __iter__(self) -> iter:
"""Returns an iterator over the message stream.
"""
return iter(self._data)
def __notify__(self, event: str, mid: int, data: any) -> None:
"""Notifies all observers of the message stream about an event until it is being consumed
or until all observers have been notified.
"""
for observer in self._observers:
if observer(event, mid, data):
break
def __getitem__(self, mid: int) -> any:
"""Returns the data of a message in the message stream.
"""
if mid not in self._data:
raise KeyError(f"The message ID {mid} is not in the message stream.")
return self._data[mid]
def __setitem__(self, mid: int, data: any) -> None:
"""Puts a message into the message stream.
"""
isUpdate: bool = mid in self._data
self._data[mid] = data
self.__notify__(self.EVENT_UPDATE if isUpdate else self.EVENT_ADD, mid, data)
def __delitem__(self, mid: int) -> None:
"""Removes a message from the message stream.
"""
if mid not in self._data:
raise KeyError(f"The message ID {mid} is not in the message stream.")
data: any = self._data[mid]
del self._data[mid]
self.__notify__(self.EVENT_REMOVE, mid, data)
def __flush__(self) -> None:
"""Flushes the message stream.
"""
self._data.clear()
self.__notify__(self.EVENT_FLUSH, 0, None)
# --- Properties --------------------------------------------------------------------------------
@property
def Handle(self) -> str:
"""Returns the handle of the message stream.
"""
return self._handle
@property
def Observers(self) -> tuple[callable]:
"""Returns a shallow copy of the observer list of the message stream.
"""
return tuple(self._observers)
@property
def Messages(self) -> dict[int, any]:
"""Returns a shallow copy of the messages of the message stream.
"""
return dict(self._data)
# --- Observer pattern -------------------------------------------------------------------------
def AddObserver(self, observer: callable) -> None:
"""Adds a delegate to the message stream that is called when the message stream is modified.
The delegate must have the signature observer(event: str, mid: int, data: any) -> bool.
Returning true will consume the event, returning false will propagate it to the next
observer. The event can be one of the EVENT_* symbols. The mid is the message ID and the data
is the message data.
"""
if not callable(observer):
raise TypeError("The observer must be a callable.")
if observer in self._observers:
raise ValueError("The observer is already registered.")
# Make sure that the observer can in general deal with the required signature.
try:
sig: inspect.Signature = inspect.signature(observer)
sig.bind(self.EVENT_ADD, 0, 0)
except Exception as e:
raise ValueError("The observer must have the signature observer(event: str, mid: int, "
"data: any) -> bool.") from e
self._observers.append(observer)
def RemoveObserver(self, observer: callable) -> None:
"""Removes a delegate from the message stream.
"""
if observer not in self._observers:
raise ValueError(f"The observer {observer} is not registered in the message "
f"stream {self}.")
self._observers.remove(observer)
# --- Message stream ---------------------------------------------------------------------------
def Flush(self) -> None:
"""Flushes the message stream.
"""
self.__flush__()
def Put(self, mid: int, data: any) -> None:
"""Puts a message into the message stream.
"""
self[mid] = data
def Get(self, mid: int, default: any = None) -> any:
"""Gets a message from the message stream.
"""
return self[mid] if mid in self else default
def Remove(self, mid: int) -> None:
"""Removes a message from the message stream.
"""
del self[mid]
# --- Static methods ---------------------------------------------------------------------------
@staticmethod
def GetStream(handle: str = "main") -> "MessageStream":
"""Returns the globally accessible message stream which has been attached to
#ATTACHMENT_POINT under the given #handle.
Doing it in this way is only necessary when we want to have a globally accessible message
stream with multiple users which cannot share a module, otherwise we can just put this
class in a file `stream.py`, declare there module level instance of this class and import
this module in all plugins/modules which need it.
```stream.py
class MessageStream:
...
MY_STREAM: MessageStream = MessageStream("my_stream")
```
```plugin.py
from stream import MY_STREAM
class MyPlugin (...):
def __init__(self):
MY_STREAM.AddObserver(self.OnMessageStreamEvent)
...
```
```other_plugin.py
from stream import MY_STREAM
class OtherPlugin (...):
def __init__(self):
MY_STREAM.AddObserver(self.OnMessageStreamEvent)
...
```
"""
attribute: str = f"{MessageStream.STREAM_ATTRIBUTE}_{handle.upper()}__"
if not hasattr(MessageStream.ATTACHMENT_POINT, attribute):
setattr(MessageStream.ATTACHMENT_POINT, attribute, MessageStream(handle))
stream: MessageStream = getattr(MessageStream.ATTACHMENT_POINT, attribute, None)
if ((stream.__class__.__qualname__ != MessageStream.__qualname__) or
(stream.Handle != handle)):
raise ValueError(f"Could not get the message stream for the handle '{handle}'.")
return stream
@staticmethod
def RemoveAllStreams() -> None:
"""Removes all globally accessible streams which have been attached to #ATTACHMENT_POINT.
Doing this can become necessary in development, as we otherwise cannot reload/update the
type when it is still in memory.
"""
for attr in dir(MessageStream.ATTACHMENT_POINT):
if attr.startswith(MessageStream.STREAM_ATTRIBUTE):
delattr(MessageStream.ATTACHMENT_POINT, attr)
if IS_DEBUG:
MessageStream.RemoveAllStreams()
# --- Implementation of a message dialog which makes use of all this -------------------------------
class MessageDialog(c4d.gui.GeDialog):
"""Realizes a dialog that sends messages using both SpecialEventAdd() and the custom message
stream implementation.
"""
# The IDs of the dialog elements.
ID_BUTTON: int = 1000
ID_MESSAGE: int = 1001
def __init__(self):
"""Initializes the dialog by attaching an observer to the default custom message stream.
"""
MessageStream.GetStream().AddObserver(self.OnMessageStreamEvent)
def CreateLayout(self) -> bool:
"""Sets up a UI to send a string message via the custom message stream.
"""
self.SetTitle("Message Dialog")
self.GroupBorderSpace(10, 10, 10, 10)
self.GroupSpace(5, 5)
self.AddEditText(self.ID_MESSAGE, c4d.BFH_SCALEFIT, initw=200)
self.AddButton(self.ID_BUTTON, c4d.BFH_CENTER, name="Send Message")
self.SetString(self.ID_MESSAGE, "Hello world!")
return True
def Command(self, cid: int, msg: c4d.BaseContainer) -> bool:
"""Sets of both a message via the custom message stream and a core message when the user
presses the "Send Message" button.
"""
if cid == self.ID_BUTTON:
# Define the message ID as the current time in seconds and the message as the text in
# the edit field.
mid: int = int(time.time())
msg: str = self.GetString(self.ID_MESSAGE)
# Send a message via the our custom message stream implementation.
stream: MessageStream = MessageStream.GetStream()
stream.Put(mid, msg)
print (f"Sent message with ID {mid} to {stream}.")
# And also invoke a core message using SpecialEventAdd() and ctypes, we just send here
# the message ID as p1 while we rely on storing the actual message in the message stream.
# We could also send more complex data here, but this can all get quite complex quite
# quickly.
if USE_CTYPES:
c4d.SpecialEventAdd(PID_MY_CORE_MESSAGE, p1=mid, p2=0)
print (f"Sent core message with the ID {PID_MY_CORE_MESSAGE} and data {mid}.")
return True
def OnMessageStreamEvent(self, event: str, mid: int, data: any) -> bool:
"""Called when a message is sent to the custom message stream implementation.
"""
print (f"An '{event}' event occurred for message with ID '{mid}' and data '{data}'.")
return False # We do not consume the event.
def CoreMessage(self, cid: int, msg: c4d.BaseContainer) -> bool:
"""Called by Cinema 4D when a core message is sent.
"""
# We receive a core message which notifies us that something has put something into our
# custom message stream, the data which is being sent to us via the core message is just the
# identifier of the message in our custom message stream. The core message data contains the
# p1 and p2 values of the SpecialEventAdd() call which we must unpack here (just p1 in our
# case).
if cid == PID_MY_CORE_MESSAGE and USE_CTYPES:
capsule: any = msg.GetVoid(c4d.BFM_CORE_PAR1)
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_int
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
p1: int = ctypes.pythonapi.PyCapsule_GetPointer(capsule, None)
# Do something with the message.
print (f"Received core message with packed p1 data: {p1}")
data: str | None = MessageStream.GetStream().Get(p1, None)
print (f"Resolved to actual payload '{data}' for code message p1 payload '{p1}'.")
return True
if __name__ == "__main__":
dlg: MessageDialog = MessageDialog()
dlg.Open(c4d.DLG_TYPE_MODAL_RESIZEABLE)
Hey @Dunhou,
I do not think that this is possible. At least not in a sane way, as there is no way to control the opacity of the text drawing background color alone. What you probably could do is:
GeUserArea
drawing region, using something like BaseBitmap.ScaleBicubic
.GeClipMap
canvas with that bitmap.GeUserArea
.Which is a bit "ehh" performance-wise (could be mitigated by caching the underlying GeClipMap
result) but will then also force you to do really everything in the GeClipMap
, loosing access to things like GeUserArea.DrawCustomButton
.
Cheers,
Ferdinand
Hey @lasselauch,
Maxime is now on vacation (for quite some time), you will have to wait for his reply.
We talked this morning about your thread, and my take was that this is all out of scope of support because at least I would neither support ctypes
nor third party code (the example you linked to). But Maxime wanted to support this, so it would be up to him. SpecialEventAdd
relies in C++ on casting your data, a concept that naturally does not translate well to Python. You can make stuff work, as the code demonstrates you linked to, but it is unnecessarily complicated and there are also quite some pitfalls in ctypes
, most importantly threading. In Python you can send two integers with SpecialEventAdd
(not very useful), or anything you can fit into an int
using struct
(also not that useful). You should explain what you are trying to do, then we maybe can tell you how to achieve that.
With all that being said, running from ctypes import pythonapi
does not freeze my MacBook with 2024.4.0. You should also clarify which version you are using, you have tagged this posting as S24
but you are talking about 2024
. But for everything ctypes
related you will have to wait for Maxime.
Cheers,
Ferdinand
Hey @ezeuz,
thank you for reaching out to us. Dynamic UIs are possible with GeDialog
but there is no data model abstraction as you might be accustomed to by newer UIs. So, you have to rebuild your UI by hand. I once showed here how to do something like that while also implementing a very simple data model.
But for freeing the children of popup buttons and combo boxes, you do not need all that, there you can just call GeDialog.FreeChildren. But just like for the larger level of fully dynamic dialog UIs, it is probably best to write yourself a small abstraction with a property MyComboItems
or something like that. Look at my code example I linked to when you are lost on what I mean with that, there I implemented a property Items
which in this case populated a list of items, but you could write something similar for your combo box content (and state).
Cheers,
Ferdinand
Hello @ezeuz,
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.
The answer to your question depends a bit on the context, what you want to do. One solution can be to use a bitmap button as pointed out by yourself and @Dunhou. And you can indeed set there an image which is not a resource as pointed out by @Dunhou.
The broader answer is that we never needed internally an image gadget, which is why there is none. Or viewed from the opposite side: When we need an image-like gadget, we tend to write specialized UIs like a bitmap button, a shader view or a image file view. The good news is that you can quite easily implement your own UI with GeUserArea, including an image control.
Cheers,
Ferdinand
Drawing an image using a bitmap button and user area side by side. While we can disable button behaviour like mouse over fading, we cannot truly control the scaling behaviour of the image in such button. With the bitmap button settings we can force a specific size which is smaller or larger than the original size, but we cannot make the image scale with the button or control its alignment within the button. In a GeUserArea
we can control all that with very little code.
"""Demonstrates how to display an image in a dialog using both a bitmap button and a custom image
gadget.
"""
import c4d
import itertools
from mxutils import CheckType
class ImageArea(c4d.gui.GeUserArea):
"""Realizes a custom image gadget that displays a bitmap.
This would have to be customized to do exactly what you want. Here I just realized an image
gadget that both scales up and down the image to fit the user area. WHich is different from
what the bitmap button does.
"""
MIN_SIZE_DEFAULT: tuple[int, int] = (50, 50)
def __init__(self, bmp: c4d.bitmaps.BaseBitmap, label: str):
"""Initializes the image gadget with the given bitmap and label.
"""
self._bitmap: c4d.bitmaps.BaseBitmap = CheckType(bmp, c4d.bitmaps.BaseBitmap)
self._label: str = CheckType(label, str)
def DrawMsg(self, x1: int, y1: int, x2: int, y2: int, msg: c4d.BaseContainer) -> None:
"""Called by Cinema 4D to draw the content of the user area.
"""
bmp: c4d.bitmaps.BaseBitmap = self._bitmap
self.DrawBitmap(bmp, # The image to draw.
x1, y1, x2, y2, # The area in the user area to draw the image.
0, 0, bmp.GetBw(), bmp.GetBh(), # The area in the image to draw.
mode=c4d.BMP_NORMAL) # The drawing flags, we signal here just fast scaling.
# Draw a label on top of the image.
textHeight: int = self.DrawGetFontHeight()
self.DrawSetTextCol(fg=c4d.Vector(1), bg=c4d.Vector(0.25))
self.DrawText(self._label, x1 + 5, y2 - textHeight - 5, flags=c4d.DRAWTEXT_HALIGN_LEFT)
def MinSize(self, msg: c4d.BaseContainer) -> c4d.Vector:
"""Called by Cinema 4D to determine the minimum size of the user area.
"""
# Here we could also implement that the user area cannot scale below the size of the image
# it wraps.
return c4d.Vector(*self.MIN_SIZE_DEFAULT, 0)
class ImageDialog(c4d.gui.GeDialog):
"""Realizes a dialog that displays an image using both a bitmap button and a custom image
gadget."""
# The IDs of the dialog elements.
ID_BITMAP_BUTTON: int = 1000
ID_IMAGE_AREA: int = 1001
def __init__(self):
"""Initializes the dialog's member variables.
"""
self._imageArea: ImageArea | None = None
@staticmethod
def GetExampleImage(height: int, width: int) -> c4d.bitmaps.BaseBitmap:
"""Renders a gradient into a bitmap of the given #height and #width.
"""
bitmap: c4d.bitmaps.BaseBitmap = CheckType(c4d.bitmaps.BaseBitmap())
if not bitmap.Init(width, height) == c4d.IMAGERESULT_OK:
raise RuntimeError("Failed to initialize the bitmap.")
for y, x in itertools.product(range(height), range(width)):
bitmap.SetPixel(x, y, int(x / width * 254.99),
int(y / height * 254.99),
255)
return bitmap
def CreateLayout(self):
"""Called by Cinema 4D to let the user populate the dialog with GUI elements.
"""
# Set the title of the dialog, the margin of the implicitly defined outmost group ("border
# space"), and the padding between elements ("space") in that outmost group.
self.SetTitle("Image Dialog")
self.GroupBorderSpace(10, 10, 10, 10)
self.GroupSpace(5, 5)
bmp: c4d.bitmaps.BaseBitmap = self.GetExampleImage(128, 256)
# Add a bitmap button to display the image. It is important here to either disable fading
# in the button or not to use SCALEFIT as a layout flag for the button, as the button
# background not covered by the bitmap will otherwise be tinted when hovered. What we cannot
# influence very well is how the image is scaled. We can enforce a specific scaling size but
# we cannot dynamically fit the image to the button size, as that is not the purpose of a
# bitmap button.
bc: c4d.BaseContainer = c4d.BaseContainer()
bc[c4d.BITMAPBUTTON_DISABLE_FADING] = True
gui: c4d.gui.BitmapButtonCustomGui = CheckType(self.AddCustomGui(
self.ID_BITMAP_BUTTON, c4d.CUSTOMGUI_BITMAPBUTTON, "",
c4d.BFH_SCALEFIT | c4d.BFV_TOP, 0, 0, bc), c4d.gui.BitmapButtonCustomGui)
gui.SetImage(bmp)
# Add a custom image gadget to display the image. Here we can do pretty much what we want
# without any restrictions. We can scale the image to fit the user area, or super impose
# text as I did here. It is important to make sure that the user area is not destroyed
# by storing it in the dialog instance as I did here with self._imageArea.
self._imageArea: ImageArea = ImageArea(bmp, "Example Image")
self.AddUserArea(self.ID_IMAGE_AREA, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT,
*ImageArea.MIN_SIZE_DEFAULT)
self.AttachUserArea(self._imageArea, self.ID_IMAGE_AREA)
return True
if __name__ == "__main__":
dlg: ImageDialog = ImageDialog()
dlg.Open(c4d.DLG_TYPE_MODAL_RESIZEABLE, defaultw=400, defaulth=400)
Hey @danniccs,
I am still somewhat doubtful about the usefulness of what you are trying to do. When I boil it down, you basically want to postpone saving a texture file next to a document when the document itself has not been saved yet and you therefore cannot yet derive your texture save path from the document, right? I personally would simply put a check into my code which forces the user to save the document, or provide a centralized "cache" path where such files could be stored.
Cinema 4D does not have the mechanism you seem to be looking for, a texture which resides in RAM until its document is saved to disk, to then serialize itself next to that document and update all URLs in the document.
MemoryFileStruct
is not the right choice here and the whole "in-memory-approach" is neither IMHO. If anything, you could use RamDiskInterface
. It is not just an in-memory file system but also realizes promises/lazy-loading with its method RamDiskInterface::CreateLazyFile. You pass there a delegate function, the creator
which is called when something tries to actually access the pointed resource. Our Asset API makes heavy use of that concept, under the hood all asset:///
URLs are ramdisk:///
URLs which only get resolved, i.e., downloaded from the server into a local cache, when the user actually tries to access them. The Asset Browser somewhat obfuscates that at it always downloads things once a user adds them to a scene. But that is not an intrinsic quality of asset:///
URLs, they by default wait to the very last moment with downloading their primary content. In the Asset API Examples: Load File Asset Manually I once showed some lower level RamDiskInterface
handling (but not exactly what you need here). So, RamDiskInterface
is somewhat similar what you are trying to do, but there are question marks for me if it is possible to do what you actually want to do. I would have to try myself. I could unpack things here now in detail, but that is not worth the effort IMHO.
There are three sensible approaches I see here:
SceneHookData
plugin which listens for MSG_DOCUMENTINFO
of type MSG_DOCUMENTINFO_TYPE_SAVE_BEFORE
or MSG_DOCUMENTINFO_TYPE_SAVEPROJECT_BEFORE
and then scans the document for elements of your plugin type X and updates all paths. The save path should already have been established when these messages are emitted. For clarity, all MSG_
messages are atom messages, i.e., messages emitted to scene elements via C4DAtom::Message
or ::MultiMessage
and received via NodeData::Message
. This is why you need a SceneHookData
plugin and not a MessageData
plugin (which can only receive core messages). I.e., you would here have a "spider in the net" which manually shuffles things into place. With this workflow you probably have the most freedoms but also the most work and danger of hitting a technical limitation.MSG_GETALLASSETS
and MSG_RENAMETEXTURES
, i.e., the Asset Inspector workflow (has nothing to do with the Asset API or the Asset Browser). Opposed to the "spider in the net" scene hook approach of (1.), here the management is decomposed into the scene elements that "own" the URLs, e.g., a ShaderData
, ObjectData
, or TagData
implementation. The relevant method is again NodeData::Message
. The big disadvantage is here that this whole pipeline is only runs when the user invokes 'Save Project with Assets ..' and not on normal save events.Find below a simple sketch for (2.) in Python. We have a quite extensive Asset API documentation which should help you with translating the example.
Cheers,
Ferdinand
Top: The script ran on a MacOS Cinema 4D instance and created a texture in the document database and then a material which uses this texture. later this document was saved as test.c4d
. Bottom: The file test.c4d
was copied to a Windows machine and loaded, the texture file was saved and migrated with the document attached asset database.
"""Demonstrates how to create a media asset stored in the asset repository which is attached to
the (loaded) document.
"""
import c4d
import maxon
import os
import itertools
from mxutils import CheckType, CheckIterable
doc: c4d.documents.BaseDocument # The currently active document.
op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`.
def CreateMediaAsset(path: str, doc: c4d.documents.BaseDocument) -> maxon.Url:
"""Stores the given texture file at #path as a media asset in the document repository of #doc.
"""
if not os.path.exists(CheckType(path, str)):
raise FileNotFoundError(f"File not found: {path}")
# Get the asset repository which is associated with the document #doc.
repo: maxon.AssetRepositoryRef = doc.GetSceneRepository(create=True)
if not repo:
raise RuntimeError("Could not access the documents repository.")
# Save the texture as a media asset in the document repository. The second return value signals
# if the asset already existed in the repository or not, we do not care here. We could also make
# it so here that the URL is more descriptive by passing/creating a meaningful asset category,
# so that we end up with an URL like `assetdb:///blah/cache/gradient_texture.png` or something like
# that. I pass here the empty Id() to StoreAssetStruct() to store the asset in the uncategorized
# section of the repository. We end up with the URL `assetdb:///gradient_texture.png`.
#
# Note: "assetdb" is actually not a scheme/protocol for URLs, but just smoke and mirrors for the
# user. The actual asset URL scheme is "asset" and it does not have this path like structure.
# The URL will look something like this:
#
# asset:///file_ae4d716acaccbe88~.png?name=gradient_texture.png&db=Scene (Untitled 1)
#
# In C++, we can translate between "asset" and "assetdb" URLs using the methods UrlInterface::
# ConvertToUiName and UrlInterface::ConvertFromUiName. Python does not have these methods but
# there is somewhere a thread in the forum where I posted a Python approximation for them.
description, _ = maxon.AssetCreationInterface.SaveTextureAsset(
maxon.Url(path), os.path.basename(path),
maxon.StoreAssetStruct(maxon.Id(), repo, repo), (), True)
return maxon.AssetInterface.GetAssetUrl(description, True)
def CreateTexture(size: tuple[int, int]) -> c4d.bitmaps.BaseBitmap:
"""Creates a new texture with the given size and fills it with a gradient.
"""
CheckIterable(size, int, tuple, minCount=2, maxCount=2)
bmp: c4d.bitmaps.BaseBitmap = CheckType(c4d.bitmaps.BaseBitmap())
if bmp.Init(size[0], size[1]) != c4d.IMAGERESULT_OK:
raise RuntimeError("Failed to initialize bitmap.")
for x, y in itertools.product(range(size[0]), range(size[1])):
color: c4d.Vector = c4d.Vector(x / size[0], y / size[1], 0)
bmp.SetPixel(x, y, int(color.x * 254.99), int(color.y * 254.99), int(color.z * 254.99))
return bmp
def main() -> None:
"""Called by Cinema 4D when the script is being executed.
"""
if not os.path.exists(__file__):
raise FileNotFoundError("You must save the script to disk before running it.")
# Create a new texture and save it to disk next to this script. We could also save the
# file to RAM with MemoryFileStruct, RamDiskInterface, and so on. But I was too lazy to
# implement it here.
bmp: c4d.bitmaps.BaseBitmap = CreateTexture((512, 512))
path: str = os.path.join(os.path.dirname(__file__), "gradient_texture.png")
if bmp.Save(path, c4d.FILTER_PNG) != c4d.IMAGERESULT_OK:
raise RuntimeError("Failed to save the texture to disk.")
# Now create a media asset in the document repository of #doc, get its URL, and finally delete
# the physical file on disk.
url: maxon.Url = CreateMediaAsset(path, doc)
os.remove(path)
# Create a new material and assign our asset texture stored in the document repository to it.
material: c4d.BaseMaterial = CheckType(c4d.BaseMaterial(c4d.Mmaterial))
shader: c4d.BaseShader = CheckType(c4d.BaseShader(c4d.Xbitmap))
material[c4d.MATERIAL_COLOR_SHADER] = shader
shader[c4d.BITMAPSHADER_FILENAME] = url.ToString()
material.InsertShader(shader)
doc.InsertMaterial(material)
c4d.EventAdd()
if __name__ == '__main__':
main()