Object-Plugin - Message() method POSTSETPARAMETER c4d.CallCommand(12147) - Freezing
-
the function bowspline in the object plugin is not neccessary , this is the part for the Bowspline class I just forget to delete it because first I did this in a class
-
@ferdinand said in Object-Plugin - Message() method POSTSETPARAMETER c4d.CallCommand(12147) - Freezing:
4d.EventAdd(c4d.EVENT_ENQUEUE_REDRAW)
I have changed the code now a bit , I do not load the profiles, the handle inside the GVO via a function , I load it in my
def ___init__(self)
into variables.
Here in the video you see the problem. (Videolink below) When I change a parameter in the gui it doesn´t update in the viewport . I have to press "A" for Redraw. That is the CallCommand I used in the Message() method
So that was the reason why I chatched the MSG_DESCRIPTION_POSTSETPARAMETERif type == c4d.MSG_DESCRIPTION_POSTSETPARAMETER: if data['descid'][0].id == PY_MY_PARAMETER: if c4d.threading.GeIsMainThread(): c4d.CallCommand(12147)
But then if I make an Instance with the node as reference and try to make the Instance editable , Cinema freezes.
So I tried your c4d.EventAdd() method ....where to call it? In the Message()?
Or can I use node.Message(c4d.MSG_UPDATE) anywhere?. I do not now. the message system is still unclear to me.
Anyways in the video you can see I can make the Instance Object editable. Cinema do not freeze because I commented this CallCommand thing out in the Message Method. Then it works. But as soon I use it , Cinema freezes.
Also you can observe in the video when I inserted the Instance Object and then change a parameter the Instance Object shows the change the node itself not :-).
That is not what I want.-
First I need to force the object to update in the viewport
-
Second I need that to work with the instance, when I make it editabel that it does not freezes Cinema .
-
-
@ferdinand
I have rewritten a bit my code , now I load the profile spline in my
__init__
method and keep hold of it in a porperty called
self.profile. It returns a tuple with two profiles.
when I call the method.frames = self.make_arch_frames(op)
to just create the frames, the profile are used there.
And when I just return the frames....and simultanously in my message I call the redraw, no everything works fine the Instance Object is editable. Cinema no longer freezes.
But when I create the handle which is also loaded in my__init__(self)
method as property it doesnt work
Why am I not able to place the handle in the hierachy of framesthe virtual structure of frames is:
I did this handle placement directly now in the self.make_arch_frames, but anyways, Cinema freezes when I make the Instance editable.
The frame profile is loaded in the same way as the handle.Ah I think this is a Bug in the Api of R23 and earlier, I opened a thread a few years ago...and cinema crashed also loading geometrie with hyperfile.....loading it into a virtual document would be the best maybe
-
This post is deleted! -
Yes that was the problem, everything works fine , but when I clone the object and try to make the clone object editable it doesn`t work with this redraw thing.
But I am not able to get my object show the change in realtime when I change the width-parameter for instance without this CallCommand(). I want it in the way like the cube object, when I change the width that it shows it directly in the viewport. -
it only works when I delete this :
dirty = op.CheckCache(hh) or op.IsDirty(c4d.DIRTYFLAGS_DESCRIPTION) if not dirty: return op.GetCache(hh)
Then I also do not need this callcommand thing and also the Cloner and everything works fine.
I am to stupid for this -
Hey @thomasb,
I did not had time to deal with your case yesterday, I will have a look today.
Cheers,
Ferdinand -
Hello @thomasb,
I would ask you to try to regulate a bit the amount of posting you create. It is not about volume of questions and ideas; you are welcome to ask many and detailed questions. But it can be a bit tricky to answer your questions when one must look back at a history of x postings where one does not know what is solved and what not. It is therefore best when you edit your last posting, removing things which have become irrelevant, and update things which have become/remain relevant - instead of creating multiple postings.
With that being said, there are some things which stand out to me in your code (the line numbers I use are related to the file 14432.py of your code.
- Since you did not provide executable code, the resources are missing, I could only read but not run your code.
- Something like
BowSpline
(L:25ff) should not be a class but a function. It causes not only memory overhead because aBowSpline
is effectively just aSplineObject
instance, but also can cause problems when parts of aWindows2023
cache are still referenced byBowSpline
instances. - My linter runs crazy with unused elements in your code , e.g., L:87-93. You might want to have a look at the concept of code linting for Python. Basically plugins for editors which tell you when you can/should remove or add something (or violate more complex rules).
- Just as with
BowSpline
you have the tendency to store nodes for long time usage (which then also makes your code hard to maintain and read), e.g.,Windows2023.extrude
(L:94), but also comes with access problems just asBowSpline
. You should view the albeit confusingly namedObjectData
more as an interface wich acts on scene data it gets passed in its various methods, rather than aCommandData
plugin which is basically a blank slate. You should not attachBaseList2D
instances to your plugin hook instance. - I think the whole thing is related to the city/house builder thing you talked with @Manuel about. Your plugin could use a bit of abstraction/encapsulation, because at least I struggle a bit with keeping up what each method does in
Windows2023
. - You should not invoke scene redraws on
MSG_DESCRIPTION_POSTSETPARAMETER
(L:313ff). This is simply not how anObjectData
plugin works. - What you do in the next condition, (L:318ff), extruding
self.extrude
when the user presses a button, must be bound to the main thread since you modify the document there. It is also not what usually anObjectData
plugin usually does: Heavily modify scene data. - You use both
ObjectData.SetOptimizeCache
(L:86) and manual cache handling (L:368) doing the same which does not make too much sense, but you seem to have already stumbled upon this yourself. - Reading whole objects in your load functions still seems unnecessary. I see now that you want to store point data, I would then just store the points. You talk about
HyperFile
reading about the class crashing here on PC. T which thread are your refering? - You read data from disk in
Windows2023.GetVirtualObjects
which should be avoided for performance and safety reasons. But you seem to also have fixed that already.
Long story short, your code seems to have grown a bit over your head. Which happens to the best of us, but it also limits the amount of support I can provide here, since even when I could run your code, I would probably struggle understanding what it does in all detail.
What I did instead, is I have implemented what I would consider a clean implementation of what you want to do - serialize and deserialize data used by an
ObjectData
hook to disk shared by all plugin instances (and documents), specifically at the example of splines. I simplified things a lot and I also usedJSON
instead ofHyperFile
, simply because I prefer using human-readble formats when possible. But usingHyperFile
would be fine too.Cheers,
FerdinandResult:
File: py-ostoredprofiles_2023.zip
Code:"""Provides an example for manually serializing data of a plugin that should be shared between plugin instances and documents. Entities: GetRailSpline: Returns a rail spline for the given parameters. GetProfileSpline: Returns a profile spline for the given parameters. SplinePresetCollection: Abstracts reading and writing spline presets from and to disk. StoredProfilesData: Realizes an object plugin hook that relies for its output on data that has been serialized to disk. """ import c4d import os import json import math import typing # --- The generator functions for the core output of the plugin, a rail and profile spline. def GetRailSpline(pointCount: int, length: float) -> c4d.SplineObject: """Returns a rail spline for the given parameters. """ spline: c4d.SplineObject = c4d.SplineObject(pointCount, c4d.SPLINEOBJECT_TYPE_LINEAR) if not spline: raise MemoryError("Could not allocate rail spline.") # Build the points between 0 and #length with #pointCount steps. points: list[c4d.Vector] = [ c4d.Vector(c4d.utils.RangeMap(i, 0, pointCount - 1, 0, length, True), 0, 0) for i in range(pointCount)] spline.SetAllPoints(points) spline.SetName("Rail") spline.Message(c4d.MSG_UPDATE) return spline def GetProfileSpline(pointCount: int, diameter: float) -> c4d.SplineObject: """Returns a profile spline for the given parameters. """ spline: c4d.SplineObject = c4d.SplineObject(pointCount, c4d.SPLINEOBJECT_TYPE_LINEAR) if not spline: raise MemoryError("Could not allocate profile spline.") # I saw some "manual" rotations in your code using the sine and cosine. It is a bit a question # of taste, but I would recommend using transforms instead. We rotate here with each iteration # in the list comprehension the vector #p with a length of #diameter around the z-axis, # resulting in a circle (when the splined is closed) p: c4d.Vector = c4d.Vector(0, diameter, 0) points: list[c4d.Vector] = [ c4d.utils.MatrixRotZ(2 * math.pi * (float(i)/float(pointCount))) * p for i in range(pointCount)] spline.SetAllPoints(points) spline[c4d.SPLINEOBJECT_CLOSED] = True spline.SetName("Profile") spline.Message(c4d.MSG_UPDATE) return spline # --- The interface which wraps the whole preset logic. class SplinePresetCollection: """Abstracts reading and writing spline presets from and to disk. The interface returns and accepts SplineObject instances via __getitem__ and __setitem__, hiding the internal data away for an internal user. handler = SplinePresetCollection(somePath, autoSave=True) # This will cause #spline to be saved to disk. handler["MyPreset"] = spline # Load the data back, handler does not store SplineObject instances, but their point data. otherSpline: c4d.SplineObject = handler["MyPreset"] There many ways how one can realize (de-)serialization of data. HyperFile has the advantage that it can natively deal with many Cinema 4D data types. Human readable formats as JSON or XML have the advantage that they are modifiable by users without specialized tools. I went here with JSON but one could also easily use HyperFile, XML, etc. It is mostly a question of taste. I am also serializing point data instead of parameters. Simply because it is the (slightly) more complex thing to do and therefore covers a broader audience. """ # Keys for an item in the internal data structure and the JSON IO. KEY_LABEL: str = "label" KEY_POINTS: str = "points" def __init__(self, path: str, autoSave: bool = True) -> None: """Initializes a collection with a preset file at #path to read and write data to. When #autosave is #True, the interface will automatically serialize itself when its has state changed. Auto-loading is not required, because all plugin instances share the same SplinePresetCollection handler below (but would also be possible if one wants to change the class bound SplinePresetCollection instance below). """ if not isinstance(path, str): raise TypeError(f"{path = }") if not isinstance(autoSave, bool): raise TypeError(f"{autoSave = }") if not os.path.exists(path): raise IOError(f"Profile preset path '{path}' does not exist.") if not os.path.isfile(path) or not path.lower().endswith("json"): raise IOError(f"Profile preset path '{path}' is not a JSON file.") self._path: str = path self._autoSave: bool = autoSave # The internal data of the collection. We store splines over their label and points in # local spline space, e.g., # # [ # {"label": "Rectangle", "points": [[-1, 1, 0], [1, 1, 0], [1, -1, 0], [-1, -1, 0]]}, # {"label": "Triangle", "points": [[-100, 200, 0], [100, 100, 0], [100, -100, 0]]} # ] # # We only handle here linearly interpolated splines with no tangents. We could of course # also store other parameters than the label of the spline, and then generate the spline # over these parameters. self._data: list[dict[str, typing.Union[str, list[list[float, int]]]]] = self.Read() def __len__(self) -> int: """Returns the number of presets in the handler instance. """ return len(self._data) def __getitem__(self, key: typing.Union[int, str]) -> c4d.SplineObject: """Returns a spline object for the preset with the index or label #key. """ if not isinstance(key, (int, str)): raise TypeError(f"Invalid key type: {key}.") # Get the data. pointData: list[list[float, int]] = None if isinstance(key, int): if key > (len(self._data) - 1): raise IndexError(f"The index {key} is out of bounds for {self}.") pointData = self._data[key][SplinePresetCollection.KEY_POINTS] else: for item in self._data: if item[SplinePresetCollection.KEY_LABEL] == key: pointData = item[SplinePresetCollection.KEY_POINTS] if pointData is None: raise KeyError(f"{key} is a key not contained in {self}.") points: list[c4d.Vector] = [c4d.Vector(*p) for p in pointData] spline: c4d.SplineObject = c4d.SplineObject(len(points), c4d.SPLINEOBJECT_TYPE_LINEAR) if not spline: raise MemoryError("Failed to allocate spline for preset data.") spline[c4d.SPLINEOBJECT_CLOSED] = True spline.SetAllPoints(points) spline.Message(c4d.MSG_UPDATE) return spline def __iter__(self) -> tuple[int, str]: """Yields the indices and labels of the splines in the collection. Point/spline data is not being yielded, use __getitem__ with either the yielded index or label for that. """ for i, item in enumerate(self._data): yield i, item[SplinePresetCollection.KEY_LABEL] def AddItem(self, spline: c4d.SplineObject, label: str) -> None: """Adds #spline to the data under #label. """ if not isinstance(spline, c4d.SplineObject): raise TypeError(f"Invalid item type: {spline = }.") self._data.append({ SplinePresetCollection.KEY_LABEL: label, SplinePresetCollection.KEY_POINTS: [[p.x, p.y, p.z] for p in spline.GetAllPoints()] }) if self._autoSave: self.Write() def Read(self) -> list[dict[str, typing.Union[str, list[list[float, int]]]]]: """Reads the preset data from the path associated with this collection. """ # Try to parse the data. try: with open(self._path, "r") as f: data: dict = json.load(f) except Exception as e: raise IOError(f"Loading profile data failed with the error: {e}") # Validate the schema of the data. One could also use JSON schema for that, but I kept it # simple here. For more complex cases manual validation is usually not a good choice. # HyperFile data would also have to be validated for being in a well-formed state, but # there it would be only bugs in the plugin which could cause malformed data, and not also # a user errors in manually editing the file. if not isinstance(data, list): raise IOError(f"Invalid profile data type: {type(data) = }") for item in data: if not isinstance(item, dict) or len(item) != 2: raise TypeError(f"Invalid item type: {item = }") if not "label" in item: raise KeyError(f"'label' key is missing in : {item = }") if not "points" in item: raise KeyError(f"'points' key is missing in : {item = }") if not isinstance(item["label"], str): raise TypeError(f"Invalid label: {item['label'] = }") if not isinstance(item["points"], list): raise TypeError(f"Invalid points: {item['label'] = }") for point in item["points"]: if not isinstance(point, list): raise TypeError(f"Invalid point data: {point = }") for component in point: if not isinstance(component, (float, int)): raise TypeError(f"Invalid point data: {point = }") return data def Write(self) -> None: """Writes the current state of the collection to disk. """ if not c4d.threading.GeIsMainThreadAndNoDrawThread(): raise RuntimeError(f"Could not save data from thread: {c4d.threading.GeGetCurrentThread()}") try: with open(self._path, "w") as f: json.dump(self._data, f) except Exception as e: raise IOError(f"Failed to serialize spline preset data: {e}") class StoredProfilesData(c4d.plugins.ObjectData): """Realizes an object plugin hook that relies for its output on data that has been serialized to disk. The plugin hook returns an Osweep object as its cache. The rail spline is being built dynamically on each cache build-event. The profiles are wrapped by a "preset" logic which writes the profile data as JSON to disk. """ # The plugin ID of StoredProfilesData and the ID for the "Profiles" drop-down parameter, # we will need it in #GetDDescription below. ID_PLUGIN: int = 1060702 DID_PROFILE_PRESETS: c4d.DescID = c4d.DescID( c4d.DescLevel(c4d.ID_PROFILE_PRESETS, c4d.DTYPE_LONG, 0)) # A class bound spline preset handler instance shared by all plugin hook instances. By binding # it to the class instead of to each StoredProfilesData hook instance, we ensure that all plugin # instances operate on the same data without having to read data back from disk. PRESET_HANDLER # acts effectively as a singleton. PRESET_HANDLER: SplinePresetCollection = SplinePresetCollection( os.path.join(os.path.dirname(__file__), "profiles.json")) def __init__(self): """Initializes the plugin hook. """ # We do not need any special cache handling, so we can SetOptimizeCache(). self.SetOptimizeCache(True) super().__init__() def Init(self, node: c4d.GeListNode) -> bool: """Called by Cinema 4D to initialize the plugin object #node. """ self.InitAttr(node, int, c4d.ID_PROFILE_PRESETS) self.InitAttr(node, float, c4d.ID_PROFILE_DIAMETER) self.InitAttr(node, int, c4d.ID_PROFILE_SUBDIVISIONS) self.InitAttr(node, float, c4d.ID_RAIL_LENGTH) self.InitAttr(node, int, c4d.ID_RAIL_SUBDIVISIONS) node[c4d.ID_PROFILE_PRESETS] = 0 node[c4d.ID_PROFILE_DIAMETER] = 50.0 node[c4d.ID_PROFILE_SUBDIVISIONS] = 12 node[c4d.ID_RAIL_LENGTH] = 500.0 node[c4d.ID_RAIL_SUBDIVISIONS] = 24 return True def GetVirtualObjects(self, op: c4d.BaseObject, hh: object) -> typing.Optional[c4d.BaseObject]: """Called by Cinema 4D to build the cache for the plugin node #op. """ # When you invoke SetOptimizeCache(True) in ObjectData.__Init__(), also applying the default # manual cache handling as shown below and done in your code makes only little sense. # Because that is then already being done by Cinema 4D for you. When one is determining the # dirty state manually, one usually also wants to deviate from the default behaviour by for # example updating the cache when an object linked in the parameters of #op is dirty, # when the current time of the document has changed, or something similarly custom. # # # Determine if the object is dirty, and if not, simply return its existing cache. Doing # # this is equivalent to invoking self.SetOptimizeCache(True) in __init__(). # dirty = op.CheckCache(hierarchyhelp) or op.IsDirty(c4d.DIRTYFLAGS_DATA) # if dirty is False: return op.GetCache(hierarchyhelp) # Get relevant parameters from the node in the scene. railLength: float = op[c4d.ID_RAIL_LENGTH] railSubdivisions: int = op[c4d.ID_RAIL_SUBDIVISIONS] presetIndex: int = op[c4d.ID_PROFILE_PRESETS] # Allocate the cache root, in this case a sweep object we are going to attach our customly # generated rail and profile splines to. root: c4d.BaseObject = c4d.BaseObject(c4d.Osweep) if not root: raise MemoryError("Could not allocate object in cache.") # Get profile spline from the preset handler and build the rail manually. profileSpline: c4d.SplineObject = StoredProfilesData.PRESET_HANDLER[presetIndex] railSpline: c4d.SplineObject = GetRailSpline(railSubdivisions, railLength) profileSpline.InsertUnderLast(root) railSpline.InsertUnderLast(root) # Return the new cache hierarchy. return root def GetDDescription(self, node: c4d.GeListNode, description: c4d.Description, flags: int) -> typing.Union[bool, tuple[bool, int]]: """Called by Cinema 4D to let a plugin modify its description GUI. We use this here to populate our "Profiles" parameter drop-down menu with the data wrapped by #StoredProfilesData.PRESET_HANDLER. """ # Make sure that the description of this plugin type has been loaded. if not description.LoadDescription(node.GetType()): return False # Only modify the #ID_PROFILE_PRESETS cycle when Cinema 4D asks us to do so, or when # Cinema 4D does not specify for which parameter this call is for. targetId: c4d.DescID = description.GetSingleDescID() if not targetId or StoredProfilesData.DID_PROFILE_PRESETS.IsPartOf(targetId): # Get the whole description container instance for the parameter #ID_PROFILE_PRESETS. bc: c4d.BaseContainer = description.GetParameterI(StoredProfilesData.DID_PROFILE_PRESETS) if not bc: return (True, flags) # Get the cycle values container instance in it and flush all items. items: c4d.BaseContainer = bc.GetContainerInstance(c4d.DESC_CYCLE) items.FlushAll() # Build the values from scratch with the items in #StoredProfilesData.PRESET_HANDLER. for index, label in StoredProfilesData.PRESET_HANDLER: items.SetString(index, label) return (True, flags | c4d.DESCFLAGS_DESC_LOADED) def Message(self, node: c4d.GeListNode, mid: int, data: object) -> bool: """Called by Cinema 4D to convey events for the plugin object #node. Used here to catch the user pressing the "Save Preset" button and saving said preset. """ # Our #ID_PROFILE_SAVE button has been pressed, we save a new preset. if (mid == c4d.MSG_DESCRIPTION_COMMAND and isinstance(data, dict) and isinstance(data.get("id", None), c4d.DescID)): if data["id"][0].id != c4d.ID_PROFILE_SAVE: return True # Bail when we are not on the non drawing main thread as we are going to open a dialog # to ask the user for a preset label. if not c4d.threading.GeIsMainThreadAndNoDrawThread(): return True label: str = c4d.gui.InputDialog( "Preset Name:", f"Preset {len(StoredProfilesData.PRESET_HANDLER) + 1}") # Build the profile spline with the current settings. diameter: float = node[c4d.ID_PROFILE_DIAMETER] subdivisions: int = node[c4d.ID_PROFILE_SUBDIVISIONS] spline: c4d.SplineObject = GetProfileSpline(subdivisions, diameter) # Store the spline in the handler, because we set #autoSave to #True, this will also # cause the data to be saved to disk. After that, we flag ourselves as description dirty, # as the drop-down menu of the "Preset" parameter must be updated. StoredProfilesData.PRESET_HANDLER.AddItem(spline, label) node.SetDirty(c4d.DIRTYFLAGS_DESCRIPTION) return True @staticmethod def Register() -> bool: """Registers the plugin hook. This method is not part of the ObjectData interface, I just like to do it like this, attach the plugin ID and registration method to the plugin hook they belong to. Could also be done differently. """ bmp: c4d.bitmaps.BaseBitmap = c4d.bitmaps.InitResourceBitmap(c4d.Osweep) if not bmp: return False return c4d.plugins.RegisterObjectPlugin(id=StoredProfilesData.ID_PLUGIN, str="Stored Profiles Object", g=StoredProfilesData, description="ostoredprofiles", icon=bmp, info=c4d.OBJECT_GENERATOR) if __name__ == "__main__": if not StoredProfilesData.Register(): print(f"Warning: Failed to register 'StoredProfilesData' in '{__file__}'.")
-
@ferdinand
sorry ferdinand for this amount of posts. I did not know that I may delete unnecessary posts.
I am writing code with PyCharm, when he complains , I improve the code.
As I said, the code is a bit messy.....Many unnecessary things that I would have improved in the end.
In the future, I will curb my euphoria a bit as far as the posts are concerned.
No, the plugin does not refer to the City Builder. This is different.
I'll try to put everything in methods and not in an extra class and try to implement some of your paradigms.Thank you, I've read a lot from you now, takes a while.
Thank you for your effort. I'm really ashamed...
Cheers Thomas -
Hey @thomasb,
no worries, don't be too concerned about it. And you should not be ashamed, that was certainly not what I wanted to convey. "Every person his or her book", the second so called law of library science, is something I truly believe in. Everyone has valid information needs and we are trying to help users on their individual journey. You should not feel discouraged, we are and were all once in your place.
But at the same time, I sometimes have to regulate a bit forum conduct as we, the Maxon SDK group, and other users are human too. And people tend to get confused, when they are being hit with a stream-of-conscious like threads were a user comments diary-style-like on his or her development state. It is not that I would not understand people are doing that. When one is in that situation, one is overwhelmed by the possible routes one can take, the amount of information to traverse. And verbalizing that does indeed help. But for someone who tries to help you or for people who later search for similar information, this "stream-of-conscious/diary" is then an obstacle rather than a help.
So, sometimes I try to regulate both interests a bit. But as I said, please do not feel discouraged.
Cheers,
Ferdinand