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
Hey @randymills,
good to hear that there seems to be no issue anymore and there is certainly no need to apologize. We first of all know how easily one can get side tracked in coding and secondly bug reporting is a service to the community and never a mistake even when it turns out to be false alarm. Better having a few false alarms than having bugs in the API or errors in the documentation! Thank your for taking the time.
As to why this happened to you is hard to say. The Maxon API (all the stuff in the maxon
package) is not as mature as the Classic API (all the stuff in the c4d
package) is in Python. So, I would not rule out some weird corner case 3000 which is buggy. But what I would also not rule out is that you might have shot yourself in the foot with type hinting. You strike me as more experienced user but I have seen less experienced users brick a Cinema 4D instance with type hinting more than once.
# Type hinting is the practice to type annotate code in Python. it is mostly decorative but
# I and some other SDK members tend to write type hinted code as it make clearer examples.
# Typed hinted statement which hints at the fact that result is of type int.
result: int = 1 + 1
# Functionally the same as (unless you use something like Pydantic).
result = 1 + 1
import maxon
# But one can shoot oneself in the foot when getting confused by the type hinting
# syntax with a colon.
# A correctly type hinted call that states that repo is of type #AssetRepositoryRef
repo: maxon.AssetRepositoryRef = maxon.Cast(maxon.AssetRepositoryRef, obj)
# We made a little mistake and replaced the colon with an equal sign, maybe because
# type hinting is new to us and the colon feels a bit unnatural to us. What we do here,
# is overwrite maxon.AssetRepositoryRef, i.e., it is now not the type AssetRepositoryRef
# anymore but whatever the return value of our expression was. I.e., we do a = b = 1
repo = maxon.AssetRepositoryRef = maxon.Cast(maxon.AssetRepositoryRef, obj)
# Our Cinema 4D instance is now permanently bricked until we restart Cinema 4D because
# we "lost" the type #maxon.AssetRepositoryRef.
Cheers,
Ferdinand
Hey @randymills,
Thank you for reaching out to us. I would not want to rule out that there is either a regression in our API or that I made a mistake when I documented this, but currently it does not look like that to me.
There is for example this thread where I do exactly what is shown in the snippet in the docs, cast an ObjectRef
to an AssetRepositoryRef
, in that case a sub type. The script still runs just fine for me (2024.5.1, Win 11), and when I inspect the relevant section, the values are also what I would expect them to be, i.e., this:
obj: maxon.ObjectRef = maxon.AssetDataBasesInterface.FindRepository(url)
if obj.IsNullValue():
raise RuntimeError(f"Could not establish repository for: {url}.")
# #FindRepository returns an ObjectRef, not an AssetRepositoryRef, we must cast it to its "full"
# form. Specifically, to a reference to a maxon.WatchFolderAssetRepositoryInterface we just
# implemented. Yay, casting Python, the dream becomes true :P
repo: maxon.WatchFolderAssetRepositoryRef = maxon.Cast(maxon.WatchFolderAssetRepositoryRef, obj)
print(f"{obj = }")
print(f"{repo = }")
will print that:
obj = maxon.ObjectRef object at 0x1bce66b2720 with the data: net.maxon.assets.repositorytype.watchfolder@0x000001BCAC5814C0<net.maxon.datatype.internedid> net.maxon.assetrepository.skipinsearch : <bool> false
<net.maxon.datatype.internedid> net.maxon.assetrepository.exportonsaveproject : <bool> true
repo = maxon.WatchFolderAssetRepositoryRef object at 0x1bda1573420 with the data: net.maxon.assets.repositorytype.watchfolder@0x000001BCAC5814C0<net.maxon.datatype.internedid> net.maxon.assetrepository.skipinsearch : <bool> false
<net.maxon.datatype.internedid> net.maxon.assetrepository.exportonsaveproject : <bool> true
On top of that comes that your code uses generally a bit flawed logic; which does not mean that there could not be a bug or an exception. Interfaces and references realize a managed memory architecture in our C++ API and are not interchangeable. The interface is the actual object held in memory, to which zero to many references can exist. The lifetime of an object is then managed by reference counting. This is very similar to what managed languages like Python, Java, C# do internally, it is just a bit more on the nose in our API. To briefly highlight the concept at the example of Python:
# This causes Python to allocate the array [0, 1, 2, 3, 4] in its internal object table and then
# create a reference (the value of id(data)) which is assigned to data.
data: list[int] = [0, 1, 2, 3, 4]
# When we now assign #data to something else, we are just drawing another reference to the actual
# data of #data, #x is the same object as #data.
x: list[int] = data
print(f"{id(data) == id(x) = }") # Will print True, another way to write this would be "data is x".
# Another popular case to demonstrate this are strings, which are also reference managed in most managed
# languages. This means each string is only uniquely held in memory. So, when we allocate "Maxon" twice,
# there is actually only one object in memory.
a: str = "Maxon"
b: str = "Maxon"
# Although we provided here a string literal in both cases, Python catches us duplicating data and
# only holds the object once in its object table.
print(f"{id(a) == id(b) = }") # Will print True.
# This is a bit deceptive, we are actually not modifying here the string which is referenced by #b,
# because strings are immutable in Python due to the "only stored once logic" shown above. When we
# would modify the actual object, we would also change the value of #a. So, Python does actually copy
# the object referenced by #b and then modifies that copy and assigns a reference to that to #b.
b += "Foo"
print(f"{id(a) == id(b) = }") # Will print False, a and b are not the same object anymore.
id(data) == id(x) = True
id(a) == id(b) = True
id(a) == id(b) = False
Interfaces are similar to the hidden entities living in Python's object table, and references are somewhat equivalent to the frontend data instances a user deals with in Python. We have a manual for Python and C++ about the subject for our API. The C++ manual is slightly better, but both unfortunately do their best to obfuscate the subject with a very technical approach rather than explaining it in laymans terms.
When we really boil all that down, then interfaces are "classes" and references are instances of that classes. That is more an analogy than an actual fact but conveys the major idea that an instances of a class are always meant to be dealt with in the form of references to some interface object.
Because of all that, it strikes me as a bit unlikely that you must cast down a reference to an interface (maxon.AssetDataBasesInterface.FindRepository
returns an ObjectRef
, not an ObjectInterface
, i.e., a reference to some data, not the actual data). Because our managed memory approach does expose that actual raw data (all the SomethingInterface
types), you can also technically use the raw object, as all the logic of that type is implemented there. But doing that is very uncommon and you also lack the logic which is provided by references like for example IsEmpty()
, IsNullValue()
, or GetType()
which is not unimportant. References derive from maxon.Data where the these methods provided by references are implemented, interfaces derive from maxon.ObjectInterface and implement the actual functionality of some type, e.g., FindAssets
for AssetRepositoryInterface
. The methods of an interface then "shine through" on its reference, because the reference is also in inheritance relation to its interface.
When you need more assistance here, or want this generally to be clarified, please share your code. There could be a bug or an inconsistency in our API, or I simply documented something incorrectly. But for me it currently looks more like that something in your code is going wrong.
Cheers,
Ferdinand
Hey @treezw,
As it turns out, there is also a special syntax with which you can do that, but it requires you registering a dummy hook (with its own ID) for each separator.
Cheers,
Ferdinand
"""Realizes a simple plugin collection with multiple command plugins.
"""
import c4d
class FooCommandData(c4d.plugins.CommandData):
"""Realizes a dummy command plugin.
"""
PLUGIN_ID: int = 1064229
PLUGIN_NAME: str = "Foo Command"
def Execute(self, doc):
print(f"Executing {self.PLUGIN_NAME}")
return True
class BarCommandData(FooCommandData):
"""Realizes a dummy command plugin.
"""
PLUGIN_ID: int = 1064230
PLUGIN_NAME: str = "Bar Command"
class BazCommandData(FooCommandData):
"""Realizes a dummy command plugin.
"""
PLUGIN_ID: int = 1064231
PLUGIN_NAME: str = "Baz Command"
class SeparatorCommandData(c4d.plugins.CommandData):
"""Realizes a dummy command plugin used by separators in menus.
Please use a command data plugin as a dummy plugin, as there are not unlimited slots for other
plugin types in Python (a user can have only 50 of each NodeData derived plugin type installed).
We can have each separator use this type but they all need to have a unique ID.
"""
SEPARATOR_ID_1: int = 1064234
SEPARATOR_ID_2: int = 1064235
def Execute(self, doc): return True
def RegisterPlugins() -> None:
"""Registers the plugins of the plugin collection.
"""
# To sort plugins in a custom order, we must use the "$#00" syntax. We can pass here a two-digit
# number for the index of the plugin in the menu. The logic we use here is that when #hook is a
# an #int, we register there a dummy plugin for a separator.
for i, hook in enumerate((FooCommandData,
SeparatorCommandData.SEPARATOR_ID_1,
BazCommandData,
SeparatorCommandData.SEPARATOR_ID_2,
BarCommandData)):
# Separators can be realized with the special syntax "$#00--", but we need a dummy plugin
# for each separator. We can have each separator use the DummyCommandData type but they all
# need to have a unique plugin ID. If you want to use separators, please use a command data
# plugin use a command data plugin as the dummy plugin, as there are not unlimited slots for
# other plugin types.
if isinstance(hook, int):
label = f"#${str(i).zfill(2)}--" # E.g., "#$01--" will be a separator at index 1
pid: int = hook
hook = SeparatorCommandData
else:
label = f"#${str(i).zfill(2)}{hook.PLUGIN_NAME}" # E.g., "#$00Foo" will be "Foo" at index 0
pid: int = hook.PLUGIN_ID
print (f"{label = }")
if not c4d.plugins.RegisterCommandPlugin(pid, label, 0, None ,None, hook()):
raise RuntimeError(f"Failed to register {hook} plugin.")
if __name__ == "__main__":
RegisterPlugins()
Hey, separators are not possible in this menu, as it is dynamically built. When you want separators, you must build your own menu.
Hello @treezw,
Thank you for reaching out to us and thank you for providing stripped down example code, we really appreciate that. What @Dunhou said is right, you can build entirely custom menus entries as shown in that thread he linked to.
But I think you want to operate within the "Extensions" menu, right? I would not say that we discourage adding new top level menu entries in general, but the default should be that plugins are only placed in the "Extensions" menu. I am not quite sure how your screen shots came to pass, as they do not line up with the demo code you gave us. But in general, all entries are alpha-numerically sorted in the Extensions and its sub-menus. So, I would not call this 'very bad' sorting, it is just how Cinema does things. When we have for example the Listing A
shown below, Cinema 4D will not care that we register the commands in the order Foo, Baz, Bar, it will put them in alphanumerical order into the menu, i.e., do this:
This assumes that there is a {SOMEPATH}\plugins\MyPluginCollection\myPluginCollection.pyp
with the code shown in Listing A
and that the user has linked {SOMEPATH}\plugins
as a plugin path in his or her plugin collection. The sub menu entry MyPluginCollection
then comes from the folder of the same name.
But we can disable this automated sorting with the #$xy{SOMELABEL}
syntax, where xy
is the two digit index of where the plugin hook should be displayed in its extensions menu. This is being shown in Listing B
and will then result in this, where we force a non-alphanumerical order:
One can also stack multiple folders in a plugin package to build complex menus in the extensions menu. E.g., this, where {SOMEPATH}\py
is a plugins directory linked in a Cinema 4D instance:
Will result in this:
There is of course some limit to which it makes sense to organize plugins in this manner. But in general, you should avoid opening new top level menu entries unless your plugin offers a s substantial feature set. This is not a technical limitation, there is no technical harm in doing that, but a user experience restriction. We want Cinema 4D to have a consistent and clean look. When every plugin creates its top-level menu entry, the menu bar of a user can become quite confusing or even overflow (become wider than one screen width). If your plugin suite mandates its own menu entry is up to you to decide but we would encourage asking yourself if that is really beneficial for your user experience.
Cheers,
Ferdinand
"""Realizes a simple plugin collection with multiple command plugins.
"""
import c4d
class FooCommandData(c4d.plugins.CommandData):
"""Realizes a dummy command plugin.
"""
PLUGIN_ID: int = 1064229
PLUGIN_NAME: str = "Foo Command"
def Execute(self, doc):
print(f"Executing {self.PLUGIN_NAME}")
return True
class BarCommandData(FooCommandData):
"""Realizes a dummy command plugin.
"""
PLUGIN_ID: int = 1064230
PLUGIN_NAME: str = "Bar Command"
class BazCommandData(FooCommandData):
"""Realizes a dummy command plugin.
"""
PLUGIN_ID: int = 1064231
PLUGIN_NAME: str = "Baz Command"
def RegisterPlugins() -> None:
"""Registers the plugins of the plugin collection.
"""
# When we place this file inside a folder named "myPluginCollection" in the plugins directory,
# linked as a plugin folder in Cinema, we will wind up with the following menu structure:
#
# Extensions
# |- ...
# |- myPluginCollection
# |- Bar Command
# |- Baz Command
# |- Foo Command
#
# This is because Cinema will sort by default all entries in the Extensions alphanumerically.
for i, hook in (FooCommandData, BazCommandData, BarCommandData):
if not c4d.plugins.RegisterCommandPlugin(
hook.PLUGIN_ID, hook.PLUGIN_NAME, 0, None ,None, hook()):
raise RuntimeError(f"Failed to register {hook.PLUGIN_NAME} plugin.")
if __name__ == "__main__":
RegisterPlugins()
"""Realizes a simple plugin collection with multiple command plugins with a custom menu order.
"""
import c4d
class FooCommandData(c4d.plugins.CommandData):
"""Realizes a dummy command plugin.
"""
PLUGIN_ID: int = 1064229
PLUGIN_NAME: str = "Foo Command"
def Execute(self, doc):
print(f"Executing {self.PLUGIN_NAME}")
return True
class BarCommandData(FooCommandData):
"""Realizes a dummy command plugin.
"""
PLUGIN_ID: int = 1064230
PLUGIN_NAME: str = "Bar Command"
class BazCommandData(FooCommandData):
"""Realizes a dummy command plugin.
"""
PLUGIN_ID: int = 1064231
PLUGIN_NAME: str = "Baz Command"
def RegisterPlugins() -> None:
"""Registers the plugins of the plugin collection.
"""
# To sort plugins in a custom order, we must use the "$#00" syntax for the plugin label. The plugin
# will not display that prefix in its name, but with the `xy` part in the prefix we can set an position
# index for the menu of the plugin. E.g., "#$05Blah Command" means that Cinema 4D will try to put the
# "Blah Command" at the 5th position in that menu.
for i, hook in enumerate((FooCommandData, BazCommandData, BarCommandData)):
# E.g, "#$00Foo Command". It is important to use the zfill method to ensure that the number
# has always two digits. We cannot do any sorting beyond 100 plugins within the same menu.
label: str = f"#${str(i).zfill(2)}{hook.PLUGIN_NAME}"
if not c4d.plugins.RegisterCommandPlugin(
hook.PLUGIN_ID, label, 0, None ,None, hook()):
raise RuntimeError(f"Failed to register {hook.PLUGIN_NAME} plugin.")
if __name__ == "__main__":
RegisterPlugins()
Hello,
Thank you for reaching out to us. @Dunhou is right, the Picture Viewer is sealed, which means that you can put images into it (with c4d.bitmaps.ShowBitmap
or c4d.documents.LoadFile
) but you cannot get images or information out of it. This generally applies to all UIs, we do not expose UIs, but the data structures behind them. So, there is for example no Object or Material Manager API interface, their functionalities are exposed via the scene graph. For the Picture Viewer there is no data structure exposed.
Cheers,
Ferdinand
Good to hear that you found your solution!
Hey @SmetK,
Thank you for reaching out to us. Please provide your code in future postings as pointed out before. Screenshots are not a substitute for code. I have provided a brief example of what you are trying to do.
Cheers,
Ferdinand
"""Constructs cone objects on the selected spline object at five different points and rotates them
by 90 degrees on their principal X and Z axes.
"""
import c4d
doc: c4d.documents.BaseDocument # The currently active document.
op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`.
# Ninety degrees in radians and two pre-built matrices for rotating 90 degrees around the X and Z
# axes. The Y axis is not too useful here since a cone is symmetrical along its Y axis.
RAD_90_DEG : float = c4d.utils.DegToRad(90)
TRANSFORM_90_X : c4d.Matrix = c4d.utils.MatrixRotX(RAD_90_DEG)
TRANSFORM_90_Z : c4d.Matrix = c4d.utils.MatrixRotZ(RAD_90_DEG)
def GetCone(doc: c4d.documents.BaseDocument) -> c4d.BaseObject:
"""Creates a new cone object.
"""
cone: c4d.BaseObject = c4d.BaseObject(c4d.Ocone)
cone[c4d.PRIM_CONE_HEIGHT] = 40
cone[c4d.PRIM_CONE_BRAD] = 10
doc.InsertObject(cone)
return cone
def main() -> None:
"""Called by Cinema 4D when the script is being executed.
"""
# Ensure that there is an editable spline object selected.
if not isinstance(op, c4d.SplineObject):
return
# Create a new SplineHelp object and initialize it with the selected spline object. Then iterate
# over five offsets (0, 0.25, 0.5, 0.75, 1) and get the matrix at each offset. A spline does not
# have unambiguously a matrix for each offset since a normal and a tangent of a point only define
# two out of the three degrees of freedom. The matrices which are constructed here are in respect
# to the #upvector we pass to the SplineHelp object.
sh: c4d.utils.SplineHelp = c4d.utils.SplineHelp()
sh.InitSplineWithUpVector(op, upvector=c4d.Vector(0, 1, 0))
for offset in (0, 0.25, 0.5, 0.75, 1):
mg: c4d.Matrix = sh.GetMatrix(offset)
# Now construct two cones on each point and rotate them 90 degrees around the X and Z axes.
# The order in matrix multiplication is important, because other than for example the natural
# or real numbers, matrix multiplication is not commutative. So X * Y is not always equal to
# Y * X in matrix multiplication. In this case we want to multiply the spline point matrix
# by our additional rotation matrices. We already operate in world coordinates, with #mg,
# so we do not need to multiply by the matrix of the spline object itself.
coneX: c4d.BaseObject = GetCone(doc)
coneZ: c4d.BaseObject = GetCone(doc)
coneX.SetMg(mg * TRANSFORM_90_X)
coneZ.SetMg(mg * TRANSFORM_90_Z)
coneX.SetName(coneX.GetName() + "X")
coneZ.SetName(coneZ.GetName() + "Z")
c4d.EventAdd()
if __name__ == '__main__':
main()
Hey everyone,
so, I did indeed overlook something here (which I apparently did correctly on MacOS without noticing). Our command line arguments are a bit non standard in that they expect an equal sign between the argument label and value instead of the commonly used whitespace. So, it is -g_encryptPypFile=myfile.pyp
and not -g_encryptPypFile myfile.pyp
. I have also fixed this in the docs, as this was also incorrect there.
So the full correct syntax would be:
> c4dpy d:\scripts\myscript.py -g_encryptPypFile=d:\plugins\py-commanddata_dialog_r13.pyp
My apologies for the inconvenience,
Ferdinand
Hey,
yes, the forum is the best place, and for normal Python API feature requests the hurdles are also bit lower since SDK, i.e., us, then does own the code
Cheers,
Ferdinand