Your browser does not seem to support JavaScript. As a result, your viewing experience will be diminished, and you have been placed in read-only mode.
Please download a browser that supports JavaScript, or enable it if it's disabled (i.e. NoScript).
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
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.
CreateLayout()
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
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.
pseudo decimals
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.
float
floor
my_float * 10 ** digits
10 ** digits
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
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
the nature of your problem remains a bit unclear to me. You talk about polygonal data and beveling edges, which I would interpret as the question for bevelling in a network of curves, .e.g. a polygonal topology, but you show the image of a curve and also your code operates in a plane. I assume you want interpret your mesh as a series of cross-sections/curves?
That aside, beveling (either curves or curves in surfaces, i.e. edges) is an application of interpolation and can therefor implemented in various forms, here are some approaches:
Piecing in arc segments. This what you are basically trying to do from the looks of your code. You normally would properly define a series of transforms (i.e. "matrices" in Cinema) to transform a base vector and by that construct the vertices making up the arc segment. This solution is not very suited for a data of intersecting curve networks, i.e. surfaces, it works better in something like a 2D Vector app.
Than there are multiple ways to construct a smooth curve over a series points. The easiest one are Chaikin's Curves which also have spawned a whole group of algorithms all following more or less the same one fourth three fourth idea and some of them can also be applied to meshes.
The next obvious candidates would be quadratic or cubic polynomials/curves. They can the can also be viewed as a combination of linear interpolations. This is called De Casteljau's algorithm. To get from multiple curves segments to splines, you just piece them together (there are multiple ways to do this). But in you case of beveling you probably will only need a single curve. Depending on how deep you venture into this topic this reaches from splines and curves (.e.g. a B-Spline or a Bezier spline) to surfaces (e.g. a NURBS surface).
The last one, and that is the one Cinema probably uses for its edge tool is just Subdivision Surfaces. There are also multiple algorithms of varying complexity, but classical Catmull-Clark SDS have very nice properties (producing only quads for example) and are very easy to implement. The problem here is how to restrict the subdivision part of the algorithm so that you only generate new edges along the edge loop and not perpendicular to it and then come up with the correct modified weights for the smoothing.
Its all a bit vague, but your question is very broad, so I thought this might help.
edit: Forgot this, but should have put this first - Implementing a bevel tool is not trivial. Covering the default case is not that hard, but covering all the corner cases is, especially when you want to maintain secondary mesh attributes like texture coordinates. I would not go for reinventing the wheel here. Is there a reason why you do not want to use Cinema's edge bevel? Does it not work with SendModellingCommand?
SendModellingCommand
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).
GetActiveDocument()
NodeData
Hoi,
@blastframe said in Moving a Point Along an Arc by Distance from another Point:
In your code comment you mention spherical interpolation between the arc segment. Is that the same as the quadratic bezier interpolation from the example you provided (code below)? I think that's also called slerp?
Yeah, slerp is a synonym for spherical linear interpolation. While linear interpolation, a.k.a. lerp, gives you points on the line segment spanned by its two input vectors, a spherical linear interpolation gives you points on the arc segment spanned between the unit vectors of the two input vectors. Cinema does not have a slerp for vectors for some reason, only one for quaternions. The whole interpolation and interpolation naming situation in Cinema's classic API is a huge mess IMHO. So you would have to write your own slerp. And slerp is not directly related to splines, only in the sense that you will need it when you want to linearly place points on that spline, as you then have to do the arc-length parametrization of that spline for which you will need slerp.
slerp
lerp
How do I get the points to stop at the limits of the arc? In this case, it would seem to be limiting the angle to 0 and math.pi, but what if the start & end angles are different?
math.pi
Basically the same way you did it in your code. The computed angle is an angle in radians. So to limit it to let's say [0°, 180°] you would have to clamp it to [0 rad,π rad]. There is also the problem that angles work the other way around in relation to the circle functions sine and cosine and your example. They "start on the right side" of the unit circle and then go counter clockwise. Which is why my code flips things around. Getting back to "your setup" should just a matter of inverting the angle, i.e. -theta instead of theta, and then adding π, i.e. 180°.
[0°, 180°]
[0 rad,π rad]
sine
cosine
If I'm not to use c4d.Vector.GetDistance, how would you recommend I get the geodesic norm? I found modules online like geopy, but I'd rather do it with standard Python.
c4d.Vector.GetDistance
Its the same thing in green. You get the Euclidean norm, a.k.a the "length" of your vectors, implying the radius of the circle they are sitting on. Then you can compute the angle spanned between them with the dot product. Finally you put this into the relation shown above, solve for the arc length and you are done. And to be clear, Geodesic norm is just a fancy name for saying the distance between two points on a sphere, while the Euclidean norm is the distance in a plane.
you have to invoke AddUserArea and then attach an instance of your implemented type to it. Something like this:
AddUserArea
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.
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 @bentraje,
thank you for reaching out to us. As @Cairyn already said, there is little which prevents you from writing your own cloner. Depending on the complexity of the cloner, this can however be quite complex. Just interpolating between two positions or placing objects in a grid array is not too hard. But recreating all the complexity of a MoGraph cloner will be. It is also not necessary. Here are two more straight forward solutions:
file: cube_bend_python.c4d code:
"""Example for modifying the cache of a node and returning it as the output of a generator. This specific example drives the bend strength of bend objects contained in a Mograph cloner object. The example is designed for a Python generator object with a specific set of user data values. Please use the provided c4d file if possible. Note: This example makes use of the function `CacheIterator()` for cache iteration which has been proposed on other threads for the task of walking a cache, looking for specific nodes. One can pass in one or multiple type symbols for the node types to be retrieved from the cache. I did not unpack the topic of caches here any further. We are aware that robust cache walking can be a complex subject and already did discuss adding such functionality to the SDK toolset in the future, but for now users have to do that on their own. As discussed in: plugincafe.maxon.net/topic/13275/ """ import c4d # The cookie cutter cache iterator template, can be treated as a black-box, # as it has little to do with the threads subject. def CacheIterator(op, types=None): """An iterator for the elements of a BaseObject cache. Handles both "normal" and deformed caches and has the capability to filter by node type. Args: op (c4d.BaseObject): The node to walk the cache for. types (Union[list, tuple, int, None], optional): A collection of type IDs from one of which a yielded node has to be derived from. Will yield all node types if None. Defaults to None. Yields: c4d.BaseObject: A cache element of op. Raises: TypeError: On argument type violations. """ if not isinstance(op, c4d.BaseObject): msg = "Expected a BaseObject or derived class, got: {0}" raise TypeError(msg.format(op.__class__.__name__)) if isinstance(types, int): types = (types, ) if not isinstance(types, (tuple, list, type(None))): msg = "Expected a tuple, list or None, got: {0}" raise TypeError(msg.format(types.__class__.__name__)) # Try to retrieve the deformed cache of op. temp = op.GetDeformCache() if temp is not None: for obj in CacheIterator(temp, types): yield obj # Try to retrieve the cache of op. temp = op.GetCache() if temp is not None: for obj in CacheIterator(temp, types): yield obj # If op is not a control object. if not op.GetBit(c4d.BIT_CONTROLOBJECT): # Yield op if it is derived from one of the passed type symbols. if types is None or any([op.IsInstanceOf(t) for t in types]): yield op # Walk the hierarchy of the cache. temp = op.GetDown() while temp: for obj in CacheIterator(temp, types): yield obj temp = temp.GetNext() def main(): """ """ # The user data. node = op[c4d.ID_USERDATA, 1] angle = op[c4d.ID_USERDATA, 2] fieldList = op[c4d.ID_USERDATA, 3] # Lazy parameter validation ;) if None in (node, angle, fieldList): raise AttributeError("Non-existent or non-populated user data.") # Get the cache of the node and clone it (so that we have ownership). cache = node.GetDeformCache() or node.GetCache() if cache is None: return c4d.BaseObject(c4d.Onull) clone = cache.GetClone(c4d.COPYFLAGS_NONE) # Iterate over all bend objects in the cache ... for bend in CacheIterator(clone, c4d.Obend): # ..., sample the field list for the bend object position, ... fieldInput = c4d.modules.mograph.FieldInput([bend.GetMg().off], 1) fieldOutput = fieldList.SampleListSimple(op, fieldInput, c4d.FIELDSAMPLE_FLAG_VALUE) if (not isinstance(fieldOutput, c4d.modules.mograph.FieldOutput) or fieldOutput.GetCount() < 1): raise RuntimeError("Error sampling field input.") # ... and set the bend strength with that field weight as a multiple # of the angle defined in the user data. bend[c4d.DEFORMOBJECT_STRENGTH] = angle * fieldOutput.GetValue(0) # Return the clone's cache. return clone
Hello @shetal,
thank you for reaching out to us. The reformulation of your question and the conformance with the forum guidelines on tagging is also much appreciated.
About your question: As stated in the forum guidelines, we cannot provide full solutions for questions, but provide answers for specific questions. Which is why I will not show here any example code, the first step would have to be made by you. I will instead roughly line out the purpose of and workflow around VideoPostData, which I assume is what you are looking for anyway.
VideoPostData
VideoPostData is derived from NodeData, the base class to implement a node for a classic API scene graph. Node means here 'something that lives inside a document and is an addressable entity', examples for such nodes are materials, shaders, objects, tags, ..., and as such 'video post' node. As mentioned in its class description, VideoPostData is a versatile plugin interface which can be used to intervene a rendering process in multiple ways. The most tangible place for VideoPostData in the app is the render settings where video post plugins can be added as effects for a rendering process as shown below with the built-in water mark video post node.
VideoPostData is an effect, meaning that you cannot use it to invoke a rendering process and on its own it also cannot forcibly add itself to a rendering and must be included manually with the RenderData, the render settings of a rendering. However, a user could make a render setting which includes such watermark effect part of his or her default render settings. One could also implement another plugin interface, SceneHookData, to automatically add such effect to every active document. We would not encourage that though, as this could be confusing or infuriating for users. Finally, such VideoPostData plugin would be visible by default like all NodeData plugins, i.e., it would appear as something in menus that the user can add and interact with. To prevent this if desired, one would have to register the plugin with the flag PLUGINFLAG_HIDE suppress it from popping up in the 'Effect ...' button menu. I cannot tell you with certainty if it is possible to hide programmatically added effect nodes from the users view in the effect list of a render settings. There are some flags which can be used to hide instances of nodes, but I would have to test myself if this also applies in this list, it is more likely that this will not be possible.
RenderData
SceneHookData
PLUGINFLAG_HIDE
To implement a VideoPostData plugin interface, one can override multiple methods and take different approaches, the most commonly used one is to override VideoPostData::Execute(Link) which will be called multiple times for each rendered frame. The method follows a flag/message logic which is commonly used in Cinema 4D's classic API, where one gets passed in a flag which signalizes in which context the method is being called. Here the context is at which state of the rendering this call is being made, and the chain is:
VideoPostData::Execute
VIDEOPOSTCALL::FRAMESEQUENCE
VIDEOPOSTCALL::FRAME
VIDEOPOSTCALL::SUBFRAME
VIDEOPOSTCALL::RENDER
VIDEOPOSTCALL::INNER
These flags are accompanied by information if the flags denotes the opening or closing of that 'step' in the rendering process. A developer often then restricts its plugin functionality to a certain flag. I.e., in your case you only want to execute some code when the closing VIDEOPOSTCALL::FRAME is being passed, i.e., after a single frame and all its sub-frames have been rendered. Execute() also passes in a pointer to a VideoPostStruct(Link) which carries information about the ongoing rendering. One of its fields is render, a pointer to a Render(Link). This data structure represents a rendering with multiple buffers and provides the method GetBuffer() which returns a pointer to VPBuffer buffer. In your case you would want to retrieve the RGBA buffer for the rendering by requesting the VPBUFFER_RGBA buffer (Link) with GetBuffer().
VideoPostStruct
render
Render
GetBuffer()
VPBuffer
VPBUFFER_RGBA
This buffer is then finally the pixel buffer, the bitmap data you want to modify. The buffer is being read and written in a line wise fashion with VPBuffer::GetLine() and ::SetLine(). Here you would have to superimpose your watermark information onto the frame. I would do this in a shader like fashion, i.e., write a function which I can query for a texture coordinate for every pixel/fragment in every line and it will then return an RBGA value which I could then combine with the RGBA information which is in the buffer at that coordinate. The details on that depend on what you want to do, e.g.,
VPBuffer::GetLine()
::SetLine()
and the answers to that are mostly algorithmic and not directly connected to our API which limits the amount of support we can provide for them. If this all sounds very confusing to you, it might be helpful to look at our video post examples I did post in the previous thread, e.g., vpreconstructimage.cpp, as this will probably make things less daunting.
If you decide that you do not want to take this route for technical or complexity reasons, you could write a SceneHookData plugin which listens via NodeData::Message for MSG_MULTI_RENDERNOTIFICATION(Link), a message family which is being sent in the context of a rendering. There you would have to evaluate the start field in the RenderNotificationData(Link) accompanying the message, to determine if the call is for the start or end of a rendering. Then you could grab the rendering output file(s) on disk with the help of the render settings from disk and 'manually' superimpose your watermark information. This will come with the drawback that you might have to deal with compressed video files like mpeg or Avi and all the image formats. Some complexity in that can be hidden away with our BaseBitmap type I did mention in my last posting, but not all of it. There is also the fact that you might run into problems when this plugin runs on a render server, where you cannot easily obtain write or even read access to files of the render output.
NodeData::Message
MSG_MULTI_RENDERNOTIFICATION
start
RenderNotificationData
BaseBitmap
I hope this give you some desired guidance, 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,
The following code example demonstrates how to discover the channel identifiers of the "Channel" parameter of a Substance shader, so that the channel can be changed programmatically for a substance asset unknown at the time of writing the script.
This question reached us via mail, but since answering it requires no confidential data, we are sharing the solution here. The "trick" is to traverse the description of the shader, as these identifiers depend on the substance.
The result (the example script will randomly select a channel, but with the data provided, channels can also be selected by their name or a substring match as for example "diffuse"):
The code:
"""Example for discovering the channels of a Substance shader. The solution is a bit hacky by traversing the description of the shader but should work. """ import c4d import random def GetSubstanceChannels(shader: c4d.BaseShader) -> dict[int:str]: """Returns all channels of the substance loaded into #shader as a dictionary of id-label pairs. """ if not isinstance(shader, c4d.BaseShader) or (shader.GetType() != c4d.Xsubstance): raise TypeError(f"{shader} is not a substance shader.") # Get the data for the "Channel" dropdown element from the description of the shader. description = shader.GetDescription(c4d.DESCFLAGS_DESC_NONE) channelData = description.GetParameter(c4d.SUBSTANCESHADER_CHANNEL) # Get the elements in the drop down menu. elements = channelData[c4d.DESC_CYCLE] if not isinstance(elements, c4d.BaseContainer): raise RuntimeError(f"Could not access Channel parameter description in {shader}.") # Pack the data into a dictionary and return it. return {id: label for id, label in elements} def main(doc: c4d.documents.BaseDocument): """ """ # Get the active material. material = doc.GetActiveMaterial() if not isinstance(material, c4d.BaseMaterial): raise RuntimeError("Please select a material.") # Get the substance shader loaded into the color channel of the material. shader = material[c4d.MATERIAL_COLOR_SHADER] channelData = GetSubstanceChannels(shader) for id, label in channelData.items(): print (f"id: {id}, label: {label}") # To select a specific channel, one would have to do a string comparison here to find keywords as # "Color" or "Metal" in the channel label. I am just randomly selecting a channel instead. channelId = random.choice(tuple(channelData.keys())) channelLabel = channelData[channelId] print (f"Setting substance to channel '{channelLabel}({channelId})'") shader[c4d.SUBSTANCESHADER_CHANNEL] = channelId c4d.EventAdd() if __name__=='__main__': main(doc)
Hi @C4DS and @Motion4D,
thanks you two, that is very kind of you. A happy new year to you too and everyone in the forum.
I cannot reproduce this neither. The interesting question would be what does print random.seed for you and is this reproduceable on your end?
print random.seed
My suspicion would be that someone or something treated random.seed() like a property instead of like a function, which then led to - with all the "Python functions are first class objects" thing - random.seed being an integer. Something like this:
random.seed()
random.seed
>>> import random >>> print(random.seed) <bound method Random.seed of <random.Random object at 0x103b62218>> >>> random.seed(12345) >>> random.seed = 12345 # 'accidently' treating it like a property >>> print(random.seed) 12345 >>> random.seed(12345) Traceback (most recent call last): File "<string>", line 1, in <module> TypeError: 'int' object is not callable
preference data is often even for native c4d features implemented as a PreferenceData plugin. You have to access that plugin then. To get there you can drag and drop description elements into the python command line, delete the __getitem__() part (the stuff in brackets), and get the __repr__ of the object. With that you can figure out the plugin ID of the corresponding BasePlugin and then access your values.
PreferenceData
__getitem__()
__repr__
BasePlugin
For your case as a script example:
import c4d from c4d import plugins # Welcome to the world of Python def main(): # Search for a plugin ID with a known str __repr__ of a BasePlugin. We got from the console: # Drag and drop: Plugins[c4d.PREF_PLUGINS_PATHS] # >>> Plugins # <c4d.BaseList2D object called 'Plugins/Plugins' with ID 35 at 0x0000028FE8157A50> pid, op = None, plugins.GetFirstPlugin() while op: if 'Plugins/Plugins' in str(op): pid = op.GetID() break op = op.GetNext() print "pid:", pid # Use that ID op = plugins.FindPlugin(pid) if op: print op[c4d.PREF_PLUGINS_PATHS] # we know the enum from the console # Execute main() if __name__=='__main__': main()
You can use then that Plugin ID in cpp to do the last part (Use that ID) there.
Use that ID
technically this is possible via c4d.utils.SendModellingCommand(). However, the related specific command ID (c4d.ID_MODELING_OUTLINE_SELECTION_TOOL) is marked as private. So there is no documentation on how to use this properly.
c4d.utils.SendModellingCommand()
c4d.ID_MODELING_OUTLINE_SELECTION_TOOL
But it is not to hard to figure out the edges that are border edges. The example below will find all outlines, mimicing c4ds functionality of letting you select a specific loop is just a matter of additional filtering I left out to keep things short.
import c4d # Welcome to the world of Python def set_outline_seleection(op): """ Sets the outline edge selection for op. Args: op (c4d.PolygonObject): The object to perform an outline selection on. """ # Return if op is not a polygon object. if not isinstance(op, c4d.PolygonObject): return # This is Cinema's version of an edge neighbor data structure nbr = c4d.utils.Neighbor() nbr.Init(op) # Get the edge selection for our object. selection = op.GetEdgeS() selection.DeselectAll() # Loop over all polygons in our object. for pid, cpoly in enumerate(op.GetAllPolygons()): # All edge point pair indices of our polygon as a list of tuples with # their local edge index. edge_point_pairs = [(0, cpoly.a, cpoly.b), (1, cpoly.b, cpoly.c), (2, cpoly.c, cpoly.d), (3, cpoly.d, cpoly.a)] # Loop over all edge point pairs in a polygon: for eid, a, b in edge_point_pairs: # Skip over "empty" triangle edges - c4d presents triangles as four # point polygons if a == b: continue # Test if the current polygon ID is the only ID associated with # with the current edge, i. e. if the edge is an outline edge. if nbr.GetNeighbor(a, b, pid) == c4d.NOTOK: # Global edge indices in Cinema are indexed as polygon # ID * 4 + edge index in the polygon. # Select the edge in our BaseSelect selection.Select(pid * 4 + eid) # Update Cinema c4d.EventAdd() # Main function def main(): """ """ set_outline_seleection(op) # Execute main() if __name__ == '__main__': main()
Hello,
here are some answers:
CTRacks
BaseList2D.GetUserDataContainer()
DescID
BaseContainer
CTrack
BaseList2D
Here is a script which does what you want. I didn't went overboard with your name matching rules, but the rest should be there.
""" I broke things into two parts: 1. get_matches() deals with building a data structure of matching DescID elements. 2. add_ctracks() then does the building of CTracks. You could probably also streamline some stuff here and there, but I tried to be verbose so that things are clear. The script also only deals with the currently selected object. """ import c4d def get_matches(): """ Returns a list of tuples of the configuration (source_descid, targets), where source_descid is a DescID for which there is a CTrack in op, and targets is a list of DescIDs that match source_descid in type and name, but there is no CTrack for them in op. """ res, user_data_container = [], op.GetUserDataContainer() """ Step through the user data container of op and find elements (sources) for which there is a CTrack in op.""" for source_descid, source_bc in user_data_container: if op.FindCTrack(source_descid) is None: continue target_descid_list = [] """ Step through the user data container again and find elements (targets) for which there is NO CTrack in op and which match the current source in type and name.""" for target_descid, target_bc in user_data_container: no_track = op.FindCTrack(target_descid) is None if not no_track: continue match_name = (source_bc[c4d.DESC_NAME][:-2] == target_bc[c4d.DESC_NAME][:-2]) match_type = type(op[source_descid]) == type(op[target_descid]) is_new = sum(target_descid in data for _, data in res) == 0 if no_track and match_type and match_name and is_new: target_descid_list.append(target_descid) res.append((source_descid, target_descid_list)) return res def add_ctracks(data): """ We copy the CTrack for each source DescID the number of target DescID which are attached to that source times back into op and set the CTrack DescID each time to the target DescID. """ for source_did, target_descid_list in data: source_ctrack = op.FindCTrack(source_did) for target_did in target_descid_list: new_ctrack = source_ctrack.GetClone() new_ctrack.SetDescriptionID(op, target_did) op.InsertTrackSorted(new_ctrack) def main(): if op is not None: data = get_matches() add_ctracks(data) op.Message(c4d.MSG_UPDATE) c4d.EventAdd() # Execute main() if __name__ == '__main__': main()
I am not familiar with any C++ screen-grab libraries, so I cannot say much.
PIL
pillow
you have to unindent op = op.GetNext() one tab or the while loop condition will always be True unless your whole document consists of spline objects.
op = op.GetNext()
while loop
True
a
b,c, d
b: a, c: a, d: a
class Node(object): """A very simple node type for a tree/trie graph. """ def __init__(self, **kwargs): """The constructor for ``Node``. Args: **kwargs: Any non-graph related attributes of the node. """ # You might want to encapsulate your attributes in properties, so # that you can validate / process them, I took the lazy route. if "name" not in kwargs: kwargs["name"] = "Node" self.__dict__.update(kwargs) self.parent = None self.children = [] self.prev = None self.next = None self.down = None def __repr__(self): """The representation is: class, name, memory location. """ msg = "<{} named {} at {}>" hid = "0x{:0>16X}".format(id(self)) return msg.format(self.__class__.__name__, self.name, hid) def add(self, nodes): """Adds one or multiple nodes to the instance as children. Args: nodes (list[Node] or Node): The nodes to add. Raises: TypeError: When nodes contains non-Node elements. """ nodes = [nodes] if not isinstance(nodes, list) else nodes # last child of the instance, needed for linked list logic prev = self.children[-1] if self.children else None for node in nodes: if not isinstance(node, Node): raise TypeError(node) node.parent = self node.prev = prev if prev is not None: prev.next = node else: self.down = node self.children.append(node) prev = node def pretty_print(self, indent=0): """Pretty print the instance and its descendants. Args: indent (int, optional): Private. """ tab="\t" * indent a = self.prev.name if self.prev else None b = self.next.name if self.next else None c = self.down.name if self.down else None msg = "{tab}{node} (prev: {prev}, next: {next}, down: {down})" print msg.format(tab=tab, node=self, prev=a, next=b, down=c) for child in self.children: child.pretty_print(indent+1) def build_example_tree(): """ """ root = Node(name="root") node_0 = Node(name="node_0") node_00 = Node(name="node_00") node_01 = Node(name="node_01") node_02 = Node(name="node_02") node_1 = Node(name="node_1") node_10 = Node(name="node_10") root.add(nodes=[node_0, node_1]) node_0.add(nodes=[node_00, node_01, node_02]) node_1.add(nodes=node_10) return root root = build_example_tree() root.pretty_print()