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 @peter_horvath,
just for verboseness, you are talking about a parameter group such as the "Base" group shown below on the Redshift Standard Material node?
On the data description level groups are referenced as IntenernedId
's, specifically over maxon::DESCRIPTION::UI::BASE::GROUPID
. Groups are also not part of the data model you are operating there with, you cannot find a node for a group in a graph (I am sure you are aware, just being verbose).
I had quick look at our code base and:
DESCRIPTION::UI::BASE::GROUPID
on the node side of things.Sometimes you can just blindly set an attribute on a node and the presenter part picks then up on that (presenter as in MVP). You can 'hack' the folding state of a port bundle like this for example. But I do not see anything similar in place here. The core problem is that you are limited to the model domain of MVP in the public API.
Will ask the node team if there is a way.
Cheers,
Ferdinand
Hey @baca,
feel free to send us a mail to sdk_support(at)maxon(dot)net
in case you cannot share code publicly. But we have to have a look at your code to see what is going on. When you say this worked before, we either have a soft regression (you did something which you should not but it somehow worked and we now broke that) or a strong one (we broke something that should work) on our hands. And for that we will need code to see what is going on.
And I would not say that the performance penalty is dramatic, it more likely will be neligable. But if everyone would do that everywhere, Cinema 4D would come to a crawl. I would say you can ship your plugin with that "fix" when it does not cause any issues for you. But we should work towards solving this properly.
Cheers,
Ferdinand
Hey @baca,
Well, "expected" is a difficult term in the context of 'not alive'. Let me first unpack things a bit:
node: c4d.BaseList2D
in your Python code, for example a field layer, then there is somewhere in the memory of Cinema 4D a block which is the original C++ data, let's call the block MEM_CPP
. The block is a ::BaseList2D
(the C++ type), not a c4d.BaseList2D
(the Python type).MEM_CPP
), a c4d.BaseList2D
at MEM_PY
(your node
) which points to that MEM_CPP
for all its operations so that you can effectively drive the C++ object from Python.A 'not alive' node now means that the Python object lost its connection to the C++ object. I.e., it is what is commonly called a dangling pointer/reference. The Python object does not know anymore to whom to forward all the instructions its gets. There are cases where this makes sense or at least is obvious. When you have for example an async dialog and you get and store all objects in the scene in that dialog as a class attribute self._objects: list[c4d.BaseList2D]
when it is opened. And then try to use that list over the lifetime of that dialog. It is obvious that this can go "out of whack" because they user could have simply deleted objects since he or she opened the dialog.
But the somewhat uncomfortable reality is that Cinema 4D also reallocates scene elements a lot. So when you have an object "Cube" in your scene, you might look at it one second and it sits at the memory location X
and then you look a second later at it and it suddenly sits at Y
. For the user nothing really has changed but in memory happend what is somewhat equal to the user manually deleting the object and then recreating it from scratch with all its settings. The details of when and why this happens are irrelevant at the end of the day. In C++ this means that you should be cautious with pointers (as the object might have been deleted) and in Python this means that object can go "dead".
You should always operate on fresh scene data. When you want to browse the field layers of some object in your scene, retrieve that object from the document for each time you want to browse the layers. In node plugins the retrieving part often becomes obsolete as we often get relevant nodes passed in for each call by Cinema 4D.
With that all being said, there is definitively also some weirdness in the Python wrapper where things tend to die without a obvious reason why, especially when passing them out of functions. This can happen when the node which is being passed out is not yet attached to a document (there is probably some bug or at least design flaw in the Python wrapper). I also have seen problems with yielding (as opposed to returning).
node
, i.e., the thing which has the FIELDS
parameter, attached to a document?2024.4
version?Without more context and code, it is hard to give a good answer here.
I have forked this question because while the follow up question was absolutely okay there, this could get lengthy or might end up being flagged as a bug. And we are then better served with our own topic.
Cheers,
Ferdinand
Hey @pyxelrigger,
sure you can use the string custom GUI. You could also use a base link or a filename field which be the more natural solutions for this. But you asked for AddEditText
and that is what I answered.
Cheers,
Ferdinand
Hello @pyxelrigger,
Thank you for reaching out to us. Please share your code in the future, especially when you have a demo. In the days of AI it is not that time consuming anymore to pump out a basic setup, but it still costs me time to write the query to ask the AI to write me a GeDialog with an edit field in it and then fix the little mistakes the AI makes.
What you want to do is not directly possible. The edit field of a dialog does not support drag and drop events for scene elements. You are comparing it with the multiline edit of the console and with a string parameter in a description, which are both entirely different things.
What you can do, is implement the drag handling yourself. At least to some degree. You cannot mimic the drag-and-drop mouse cursor because the edit field over-rules you there. You can only set the drag cursor when you implement the gadget yourself. You could implement for example a text field with a GeUserArea
which lets you both type and drag, and then also displays the correct drag cursor.
Here is a quick and dirty version, which simply handles the drag event on a dialog level.
Cheers,
Ferdinand
"""Demonstrates how to handle drag events in a GeDialog.
"""
import c4d
from c4d import gui
class MyDialog(gui.GeDialog):
"""A simple dialog that displays a text edit field.
"""
ID_TEXTEDIT: int = 1000
def CreateLayout(self) -> bool:
"""Called by Cinema 4D to let the dialog populate its controls.
"""
self.SetTitle('My Dialog')
self.AddEditText(MyDialog.ID_TEXTEDIT, c4d.BFH_SCALEFIT, 200, 26)
return True
def InitValues(self) -> bool:
"""Called by Cinema 4D to let the dialog initialize its controls.
"""
self.SetString(MyDialog.ID_TEXTEDIT, "Initial text")
return True
def Message(self, msg: c4d.BaseContainer, result: c4d.BaseContainer) -> bool:
"""Called by Cinema 4D to convey events to the dialog.
We use it here to handle drag events.
"""
# This is an on-going drag event, i.e., the user is actively dragging something over the
# dialog and the user is hitting the edit text field.
if (msg.GetId() == c4d.BFM_DRAGRECEIVE and
self.CheckDropArea(MyDialog.ID_TEXTEDIT, msg, True, True)):
# Set the cursor: Does not work because we do not own the gadget implementation, the
# edit text field overwrites our cursor.
# self.SetDragDestination(c4d.MOUSE_POINT_HAND)
# We unpack the drag data from the drag message. The drag data is a dictionary with two
# keys: 'type' and 'object'. The 'type' key holds the type of the drag data (e.g.,
# files). The 'object' key holds the actual drag data (e.g., a list of files).
dragData: dict = self.GetDragObject(msg)
dragType: int = dragData.get('type', c4d.NOTOK)
dragObject: any = dragData.get('object', None)
if dragType == c4d.NOTOK or dragObject is None:
return c4d.gui.GeDialog.Message(self, msg, result)
text: str | None = None
# An atom array (a C++ thing which does not exist in Python) is a list of scene elements.
# Objects, tags, layers, shaders, materials, etc. are currently dragged over the dialog.
if dragType == c4d.DRAGTYPE_ATOMARRAY:
text = (", ".join([obj.GetName() for obj in dragObject])
if len(dragObject) > 1 else dragObject[0].GetName())
# Unknown file types, images, or scenes are currently dragged over the dialog.
if dragType in (c4d.DRAGTYPE_FILES, c4d.DRAGTYPE_FILENAME_IMAGE,
c4d.DRAGTYPE_FILENAME_OTHER, c4d.DRAGTYPE_FILENAME_SCENE):
dragObject = list(dragObject) if not isinstance(dragObject, list) else dragObject
text = (", ".join(dragObject) if len(dragObject) > 1 else dragObject[0])
# We set the text in the edit text field when we had a valid drag event.
if text is not None:
self.SetString(MyDialog.ID_TEXTEDIT, text)
return c4d.gui.GeDialog.Message(self, msg, result)
dialog: MyDialog = MyDialog()
if __name__=='__main__':
dialog.Open(dlgtype=c4d.DLG_TYPE_ASYNC, xpos=-2, ypos=-2, defaultw=400, default=400)
Hm,
okay, so you say this Vray plugin node does not use disk level 0 in its 2024 version anymore? I did not check your registration call, my alarm bells just went off when I saw the 0 because even a 2023 Vray plugin should not have a disk level of zero. Given how well established VRay is, I would expect here at least a disk level of 1 or 2 for this particular node. Did you have major rewrite of Vray or has this node been particularly uneventful in its change history that it still had a disk level of 0 in 2023?
And this is not about the disk level code you write in your code, but the branching Cinema 4D takes when there is a disk level of zero. A lot of code simply does not run when your current disk level is zero. But when you say your plugin has now a disk level of one, or that you generally increment your disk levels properly, this all does not really matter.
The Tech team touched one part of the non-public underpinning of BaseLink
for 2024. But I currently do not see how this could only impact links that are the values of orphaned parameters. Will have a look next week again.
Cheers,
Ferdinand
Hello @supermije,
Welcome to the Maxon developers forum and its community, it is great to have you with us!
Before creating your next postings, we would recommend making yourself accustomed with our forum and support procedures. You did not do anything wrong, we point all new users to these rules.
It is strongly recommended to read the first two topics carefully, especially the section Support Procedures: Asking Questions.
This forum is about development support, not end-user issues. Please use our Support Center for all end user issues.
Cheers,
Ferdinand
Hello @BruceC,
Thank you for reaching out to us. There are as far as I know no options to add custom build events with the public project tool.
For the source processor there are the sourceprocessor.buildevent
files which contain the XML for the source processor build event. But for Xcode there is no equivalent and I doubt that you can extend this for VS in a manner that you can add your own event files, I would assume that the source processor event is hardcoded.
When you really have to customize your projects in this manner, you have two options:
While 2. can probably be done reasonably fast with some knowledge about VS and Xcode project files, I would point out that the days of the source processor (at least in this form) are counted. I cannot give any ETAs here, but I would avoid putting time into custom project tool solutions, when you are not uncomfortable with this work-time becoming obsolete in a year or so. For me this would be three days. I could probably write "something" in that time, not sure though if it would be good enough for production, as these "tools" tend to cost more time than one plans for .
Cheers,
Ferdinand
Hello @dgould,
Thank you for reaching out to us. Unfortunately, your question has been off topic for the forum you have posted in. Please check the Forum Overview for an overview of the scope of the different forums.
As a result, your posting has been moved into General Talk.
The SDK team cannot make any statements about the roadmap of Cinema 4D. For questions about current and possibly future features of Cinema 4D, please reach out to us via our Support Center.
Thank you for your understanding,
Ferdinand
Hey @gheyret,
okay, I had a brief look, and I could not get the shader deserialization to work either. Not sure why, I think I was passing the wrong disk level. This is too much of a complex problem as that I could answer this on the side, we will have a closer look after our company meeting.
I would recommend to use c4d
files in the mean time to store your shaders as materials in a file. Alternatively, you could also store them as assets if you want to get all fancy.
Cheers,
Ferdinand