Object-Plugin - Message() method POSTSETPARAMETER c4d.CallCommand(12147) - Freezing
-
Hello @thomasb,
Thank you for reaching out to us. On the long run, it is best to share executable code here with us, as this will be otherwise more guess work than everything else for us. As always, you can share data confidentially with us as line out in the forum guidelines.
Here are some points:
NodeData
hooks, especially things likeObjectData
, should not enqueue scene redraw events. You can make it work when you are careful, but it simply should not be necessary as Cinema 4D does handle redraws for you.- When the plugin of this thread is identical to the plugin in this thread of yours, the reason for you trying to do this might be that you are trying to modify the cache of your plugin, the data that is referenced by
self.frame
. Doing this is not supported, caches are static by definition. Modifying it anyways will then entail having to manually force a redraw upon the change event. Doing this is not supported. - When you are really determined doing this anyways, you could try to only draw on the drawing main thread.
if (GeIsMainThread() and not GeIsMainThreadAndNoDrawThread())
. But I would not recommend doing it. - I would recommend using
c4d.EventAdd(c4d.EVENT_ENQUEUE_REDRAW)
(slower since one is doing things potentially twice, but also safer since there can only be one additional redraw enqueued, i.e., one cannot completely saturate the system) orc4d.EventAdd(c4d.EVENT_FORCEREDRAW)
(less safe, you can end up in constantly restarting an ongoing redraw when not careful). But that is all very hypothetical since aMSG_DESCRIPTION_POSTSETPARAMETER
event should not entail a manual redraw.
For the other issues you mention here,
op
being a null reference and the make editable behavior your plugin, I will have to look at your code to tell you anything concrete; but they are likely tied to (2.).Cheers,
Ferdinand -
@ferdinand
Details ansehen
1.211 / 5.000
Übersetzungsergebnisse
Übersetzung
I'm sorry Ferdinand for being such a stupid person. I haven't been programming plugins that long and I have to be honest, the road to get there was really rocky, so getting into the API wasn't particularly easy for me. I had to fight through it myself.
But no matter here you have a test code.
It's all still a bit muddled, especially as far as the description IDs are concerned, sometimes they don't initialize, well, it doesn't matter. I wasn't so aware of the self.parameter.
Well, the class BowSpline calculates the spline, other splines are supposed to be calculated later in the same class with other methods. I could also do this with functions?Well the problem is it doesn't update the spline in the viewport when I change a parameter in the description even though it breaks. Then I thought I'd catch that with POSTSETPARAMETER and call a redraw.
Oh, the problem here with the instances is that if I want to make them editable, Cinema freezes.
I load the profile spline and the handle in the GVO using a method which loads the hyperfile..
I could possibly do that in my init_(self) method.
here is the code: as already said something messi yet.import c4d from c4d import plugins, bitmaps import os import math # import copy PLUGIN_ID = 223456789 doc = c4d.documents.GetActiveDocument() HANDLE_X = 7 HANDLE_Z = 8.6 PY_WINDOW_LIST = 10002 PY_WINDOW_WIDTH = 10101 PY_WINDOW_HEIGHT = 10102 PY_ARCH_HEIGHT = 10103 PY_ARCH_SUBD = 10104 PY_OFFSET = 10105 PY_TILT_CHECK = 10106 PY_WINDOW_OPEN = 10107 PY_WINDOW_DIRECTION = 10108 PY_BANK = 10109 class BowSpline(): def __init__(self, width, height, arch, subd): self.spline = c4d.BaseObject(c4d.Ospline) self.spline[c4d.SPLINEOBJECT_CLOSED] = True self.height = height self.width = width self.arch_height = arch if self.arch_height > self.width / 2: self.arch_height = self.width / 2 if self.height < self.arch_height: self.height = self.arch_height self.subd_points = subd self.create() def create(self): if self.subd_points % 2 != 0: pass elif self.subd_points % 2 == 0: if self.subd_points >= 0: self.subd_points = self.subd_points + 1 # else: # self.subd_points = 0 if self.arch_height == 0: self.subd_points = 0 points = self.subd_points + 4 self.spline.ResizeObject(points) y_value = self.height - self.arch_height self.spline.SetPoint(1, c4d.Vector(self.width, 0, 0)) self.spline.SetPoint(2, c4d.Vector(self.width, y_value, 0)) self.spline.SetPoint(points - 1, c4d.Vector(0, y_value, 0)) if self.subd_points >= 0: if self.arch_height != 0: radius = (4 * (self.arch_height ** 2) + self.width ** 2) / (8 * self.arch_height) else: radius = 0 midpoint_pos = c4d.Vector(self.width / 2, self.height - radius, 0) rv_corner = self.spline.GetPoint(2) - midpoint_pos rv_right = c4d.Vector(self.width, midpoint_pos.y, 0) - midpoint_pos first_point_angle = c4d.utils.GetAngle(rv_right, rv_corner) last_point_angle = c4d.utils.DegToRad(180 - c4d.utils.RadToDeg(first_point_angle)) angle_adder = (c4d.utils.DegToRad(180) - (2 * first_point_angle)) / (self.subd_points + 1) angle = first_point_angle + angle_adder for index in range(3, points - 1): sine, cose = c4d.utils.SinCos(angle) y = sine * radius x = cose * radius new_point = c4d.Vector(midpoint_pos.x + x, midpoint_pos.y + y, 0) self.spline.SetPoint(index, new_point) angle += angle_adder class Windows2023(plugins.ObjectData): def __init__(self): self.pos = 5 # index where to position the window axis self.SetOptimizeCache(True) self.profile = None self.frame_spline = None self.window_spline = None self.new_doc = c4d.documents.BaseDocument() self.spline = c4d.BaseObject(c4d.Ospline) self.frame_sweep = c4d.BaseObject(c4d.Osweep) self.window_sweep = c4d.BaseObject(c4d.Osweep) self.extrude = None self.load_profile() self.start = True def Init(self, op): # self.SetOptimizeCache(True) self.InitAttr(op, int, c4d.PY_WINDOW_LIST) op[c4d.PY_WINDOW_LIST] = 1 self.InitAttr(op, float, c4d.PY_WINDOW_WIDTH) op[c4d.PY_WINDOW_WIDTH] = 100 self.InitAttr(op, float, c4d.PY_WINDOW_HEIGHT) op[c4d.PY_WINDOW_HEIGHT] = 120 self.InitAttr(op, float, c4d.PY_ARCH_HEIGHT) op[c4d.PY_ARCH_HEIGHT] = 20 self.InitAttr(op, int, c4d.PY_ARCH_SUBD) op[c4d.PY_ARCH_SUBD] = 12 self.InitAttr(op, bool, 10106) op[10106] = False self.InitAttr(op, float, 10107) op[10107] = 0.0 self.InitAttr(op, bool, 10108) op[10108] = False self.InitAttr(op, float, 10109) op[10109] = 0.0 # self.InitAttr(op, float, c4d.PY_OFFSET) # op[c4d.PY_OFFSET] = 0.0 return True def load_profile(self): # c4d.documents.MergeDocument(self.new_doc, f"{path}/res/profile.c4d", c4d.SCENEFILTER_OBJECTS) frame_path = os.path.join(path, "res", "w_f") window_path = os.path.join(path, "res", "w_w") frame_profile = c4d.BaseObject(c4d.Ospline) window_profile = c4d.BaseObject(c4d.Ospline) c4d.storage.ReadHyperFile(None, frame_profile, frame_path, 100) c4d.storage.ReadHyperFile(None, window_profile, window_path, 100) return frame_profile, window_profile def load_handle(self): handle_path = os.path.join(path, "res", "w_h") handle = c4d.BaseObject(c4d.Opolygon) c4d.storage.ReadHyperFile(None, handle, handle_path, 100) return handle def load_sill(self): sill_path = os.path.join(path, "res", "b_al") sill = c4d.BaseObject(c4d.Ospline) c4d.storage.ReadHyperFile(None, sill, sill_path, 100) return sill def make_sill(self, op): extrude: c4d.BaseObject = c4d.BaseObject(c4d.Oextrude) extrude[c4d.EXTRUDEOBJECT_EXTRUSIONOFFSET] = - op[c4d.PY_WINDOW_WIDTH] sill = self.load_sill() vector_z = op[PY_BANK] if vector_z < 0: vector_z *= -1 for point in range(sill.GetPointCount()): if point == 6 or point == 7: continue if sill.GetPoint(point).z >= 0: vector_z = 0 sill.SetPoint(point, sill.GetPoint(point) - c4d.Vector(0, 0, vector_z)) vector_z = op[PY_BANK] sill.InsertUnder(extrude) extrude = c4d.utils.SendModelingCommand(command=c4d.MCOMMAND_CURRENTSTATETOOBJECT, doc=op.GetDocument(), list=[extrude])[0] for tag in extrude.GetTags(): if tag.IsInstanceOf(c4d.Tpolygonselection) or tag.IsInstanceOf(c4d.Tedgeselection): tag.Remove() extrude.SetName("Sill") extrude.SetPhong(on=True, anglelimit=True, angle=c4d.utils.DegToRad(60)) return extrude def bow_spline(self, width, height, arch, subd): spline = c4d.BaseObject(c4d.Ospline) spline[c4d.SPLINEOBJECT_CLOSED] = True if width != 0: pass elif subd % 2 == 0: if subd >= 0: subd += 1 # else: # self.subd_points = 0 if arch == 0: subd = 1 points = subd + 4 spline.ResizeObject(points) # if self.arch_height > self.width/ 2: # # self.arch_height = self.width/ 2 # if self.width < self.arch_height: # self.width = self.arch_height y_value = height - arch spline.SetPoint(1, c4d.Vector(width, 0, 0)) spline.SetPoint(2, c4d.Vector(width, y_value, 0)) spline.SetPoint(points - 1, c4d.Vector(0, y_value, 0)) # b_height_pos = c4d.Vector(width / 2, height + b_height, 0) # B = 4 H = 5 if subd > 0: if arch != 0: radius = (4 * (arch ** 2) + width ** 2) / (8 * arch) else: radius = 0 bogen_height = arch midpoint_pos = c4d.Vector(width / 2, height - radius, 0) rv_corner = spline.GetPoint(2) - midpoint_pos rv_right = c4d.Vector(width, midpoint_pos.y, 0) - midpoint_pos first_point_angle = c4d.utils.GetAngle(rv_right, rv_corner) last_point_angle = c4d.utils.DegToRad(180 - c4d.utils.RadToDeg(first_point_angle)) angle_adder = (c4d.utils.DegToRad(180) - (2 * first_point_angle)) / (subd + 1) angle = first_point_angle + angle_adder for index in range(3, points - 1): sine, cose = c4d.utils.SinCos(angle) y = sine * radius x = cose * radius new_point = c4d.Vector(midpoint_pos.x + x, midpoint_pos.y + y, 0) spline.SetPoint(index, new_point) angle += angle_adder return spline def make_glass(self, width, height, arch=0, subd=0): spline = BowSpline() def make_arch_frames(self, node): # self.new_doc.Flush() if node[10108]: self.pos = 38 #the index of the frame where to position the axis else: self.pos = 17 frame_spline = BowSpline(node[c4d.PY_WINDOW_WIDTH], node[c4d.PY_WINDOW_HEIGHT], node[c4d.PY_ARCH_HEIGHT], node[c4d.PY_ARCH_SUBD]).spline frame_sweep = c4d.BaseObject(c4d.Osweep) frame_sweep.SetName("Frame") frame_spline.InsertUnder(frame_sweep) window_spline = BowSpline(node[c4d.PY_WINDOW_WIDTH], node[c4d.PY_WINDOW_HEIGHT], node[c4d.PY_ARCH_HEIGHT], node[c4d.PY_ARCH_SUBD]).spline window_sweep = c4d.BaseObject(c4d.Osweep) window_sweep.SetName("Window") window_spline.InsertUnder(window_sweep) profiles = self.load_profile() frame_profile = profiles[0] frame_profile.InsertUnder(frame_sweep) window_profile = profiles[1] window_profile.InsertUnder(window_sweep) frame_sweep: c4d.BaseObject = c4d.utils.SendModelingCommand(command=c4d.MCOMMAND_CURRENTSTATETOOBJECT, list=[frame_sweep], doc=node.GetDocument())[0] window_sweep: c4d.BaseObject = c4d.utils.SendModelingCommand(command=c4d.MCOMMAND_CURRENTSTATETOOBJECT, list=[window_sweep], doc=node.GetDocument())[0] frame_sweep.SetPhong(on=True, anglelimit=True, angle=c4d.utils.DegToRad(60)) window_sweep.SetPhong(on=True, anglelimit=True, angle=c4d.utils.DegToRad(60)) pos = window_sweep.GetPoint(self.pos) window_axis = c4d.BaseObject(c4d.Onull) window_axis.SetName("Axis") container = c4d.BaseObject(c4d.Onull) frame_sweep.InsertUnder(container) window_axis.InsertUnder(container.GetDown()) window_axis.SetAbsPos(pos) window_sweep.InsertUnder(window_axis) window_sweep.SetRelPos(container.GetAbsPos() - pos) if node[10105]: container.GetDown().SetRelPos(c4d.Vector(0, 0, node[10105])) if node[10106] is not None: if node[10106]: if self.pos == 17: if node[10107] is not None: window_axis.SetRelRot(c4d.Vector(0, c4d.utils.DegToRad(-5), 0)) else: if node[10107] is not None: window_axis.SetRelRot(c4d.Vector(0, c4d.utils.DegToRad(-5), 0)) else: if self.pos == 38: if node[10107] is not None: window_axis.SetRelRot(c4d.Vector(-node[10107], 0, 0)) else: if node[10107] is not None: window_axis.SetRelRot(c4d.Vector(node[10107], 0, 0)) #self.new_doc.Flush() return container # return frame, window def Message(self, node, type, data): # Arch Bow Settings # if node[c4d.PY_WINDOW_LIST] == 1: if type == c4d.MSG_DESCRIPTION_POSTSETPARAMETER: if c4d.threading.GeIsMainThread(): c4d.CallCommand(12147) if type == c4d.MSG_DESCRIPTION_COMMAND: if data['id'][0].id == c4d.PY_CREATE_BOOL: self.extrude = c4d.BaseObject(c4d.Oextrude) self.extrude.SetName("Boole") self.extrude[c4d.ID_BASEOBJECT_VISIBILITY_RENDER] = 1 tag = self.extrude.MakeTag(c4d.Tdisplay) tag[c4d.DISPLAYTAG_AFFECT_DISPLAYMODE] = True tag[c4d.DISPLAYTAG_SDISPLAYMODE] = 6 tag[c4d.DISPLAYTAG_WDISPLAYMODE] = 0 frame_spline = BowSpline(node[c4d.PY_WINDOW_WIDTH], node[c4d.PY_WINDOW_HEIGHT], node[c4d.PY_ARCH_HEIGHT], node[c4d.PY_ARCH_SUBD]).spline frame_spline.InsertUnder(self.extrude) self.extrude = c4d.utils.SendModelingCommand(command=c4d.MCOMMAND_CURRENTSTATETOOBJECT, doc=node.GetDocument(), list=[self.extrude])[0] null = c4d.BaseObject(c4d.Onull) null.SetName("Window_Bool Axis") extrude: c4d.BaseObject = self.extrude.GetDown() extrude.SetName("Window-Boole") # del tags for tag in extrude.GetTags(): if tag.IsInstanceOf(c4d.Tpolygonselection) or tag.IsInstanceOf(c4d.Tedgeselection): tag.Remove() extrude[c4d.ID_BASEOBJECT_VISIBILITY_RENDER] = 1 extrude.InsertUnder(null) self.extrude = None extrude.SetRelPos(extrude.GetRelPos() - c4d.Vector(0, 0, 10)) null.SetMg(node.GetMg()) node.GetDocument().InsertObject(null) c4d.EventAdd() elif data['id'][0].id == 10004: c4d.CallCommand(5126) return True def GetVirtualObjects(self, op, hh): # dirty = True if a cache is dirty or if the data (any parameters) of the object changed. # If nothing changed and a cache is present, return the cache # cache deaktiveren dirty = op.CheckCache(hh) or op.IsDirty(c4d.DIRTYFLAGS_DESCRIPTION) if not dirty: return op.GetCache(hh) """Settings for Arch Window""" if op[c4d.PY_WINDOW_LIST] == 1: frames = self.make_arch_frames(op) handle = self.load_handle() handle.InsertUnder(frames.GetDown().GetDown().GetDown()) sill = self.make_sill(op) sill.InsertUnder(frames.GetDown()) if op[PY_WINDOW_DIRECTION]: handle.SetRelPos(c4d.Vector(HANDLE_X, op[c4d.PY_WINDOW_HEIGHT] / 2, HANDLE_Z)) else: handle.SetRelPos(c4d.Vector(op[c4d.PY_WINDOW_WIDTH] - HANDLE_X, op[c4d.PY_WINDOW_HEIGHT] / 2, HANDLE_Z)) return frames elif op[c4d.PY_WINDOW_LIST] == 2: return c4d.BaseObject(c4d.Ocube) def GetDDescription(self, op, description, flags): if not description.LoadDescription(op.GetType()): return False single_id = description.GetSingleDescID() arch_height = c4d.DescID(c4d.PY_ARCH_HEIGHT) # using ID from above post height = c4d.DescID(c4d.PY_WINDOW_HEIGHT) if single_id is None or arch_height.IsPartOf(single_id)[0]: db = description.GetParameterI(c4d.PY_ARCH_HEIGHT) db.SetFloat(c4d.DESC_MAX, op[c4d.PY_WINDOW_WIDTH] / 2) if op[c4d.PY_ARCH_HEIGHT] is not None: if single_id is None or height.IsPartOf(single_id)[0]: db2 = description.GetParameterI(c4d.PY_WINDOW_HEIGHT) db2.SetFloat(c4d.DESC_MIN, op[c4d.PY_ARCH_HEIGHT]) group_1 = c4d.DescID(c4d.PY_GROUP_1) if single_id is None or group_1.IsPartOf(single_id)[0]: if op[c4d.PY_WINDOW_LIST] != 1: db = description.GetParameterI(c4d.PY_GROUP_1) db.SetBool(c4d.DESC_HIDE, True) return (True, flags | c4d.DESCFLAGS_DESC_LOADED) # def GetDEnabling(self, op, did, t_data, flags, itemdesc): # # if did[0].id == c4d.PY_ANIMATION_DISTANCE and op[c4d.PY_LED_MODE] == 0: # return False # elif did[0].id == c4d.PY_VERT_ANIMATION_DISTANCE and op[c4d.PY_LED_MODE] == 0: # return False # elif did[0].id == c4d.PY_PANEL_LENGTH and op[c4d.PY_LED_MODE] == 0: # return False # elif did[0].id == c4d.PY_VERT_ANIMATION_DISTANCE and op[c4d.PY_LED_MODE] == 2: # return False # # return True if __name__ == "__main__": path, file = os.path.split(__file__) files = "icon.tif" new_path = os.path.join(path, "res", files) bitmap = bitmaps.BaseBitmap() bitmap.InitWith(new_path) plugins.RegisterObjectPlugin(id=PLUGIN_ID, str="Windows2023", g=Windows2023, description="windows2023", icon=bitmap, info=c4d.OBJECT_GENERATOR)
-
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