Saving custom property class

I've defined a custom property class, which inherits from DzProperty and defined a custom widget class to display it, and it seems to work OK until I go to save the scene with one of these custom properties attached. I get "Failed to save a file. See the log for more details". The log does not contain more details - it literally only has the message telling me to see the log for more details.

What do I need to do to allow saving and loading of custom property classes?

Comments

  • OmnifluxOmniflux Posts: 377

    I don't have any experience with this, but I suspect you need to look at the Custom Elements Sample in the SDK Documentation. Specifically, mycustomshape.{cpp,h}

  • surrealsurreal Posts: 168

    As Omniflux implies, I think your problem is actually with Saving and Loading your custom widget and not with Saving and Loading the property.

    It may help you if you base your custom widget on an existing DzPropertyWgt sub class. You may need to delve into defining specific IO functionality if your custom widget class is significantly different from one of theDzPropertyWgt sub classes  i.e.define the Json serialization for your widget object

     

  • toby.allsopptoby.allsopp Posts: 20
    edited July 2022

    Hmm, I'm definitely missing something. My custom widget class doesn't have any state that would need to be saved or loaded - it gets instantiated by Daz when the property needs to be displayed. My custom widget class inherits from DzStyledPropertyWgt.

    Assuming I do need to defined JSON serialization for the widget class, how would I go about that? The examples use specific macros like DZ_PLUGIN_REGISTER_GEOMETRY_EXTRA_OBJECT_IO and I can't see one that seems applicable to properties or property widgets.

    Currently I have:

    class OcGraphProperty : public DzProperty {...};class OcGraphPropertyIO : public DzAssetExtraObjectIO {...};class OcGraphPropertyWgt : public DzStyledPropertyWgt {...};DZ_PLUGIN_CLASS_GUID(OcGraphProperty, A8B839DD-FF25-4529-B6F8-8C624BD6DF71);DZ_PLUGIN_CLASS_GUID(OcGraphPropertyIO, A8B839DD-FF25-4529-B6F8-8C624BD6DF72);DZ_PLUGIN_CLASS_GUID(OcGraphPropertyWgt, A8B839DD-FF25-4529-B6F8-8C624BD6DF73);DZ_PLUGIN_REGISTER_MODIFIER_EXTRA_OBJECT_IO(OCTANERENDER_NAME + " Node Graph", OcGraphPropertyIO, OcGraphProperty);

    It doesn't seem right that I'm using DZ_PLUGIN_REGISTER_MODIFIER_EXTRA_OBJECT_IO there, but I don't know what I should use instead.

    Edit: I originally had the IO class and the property class around the wrong way in the register macro, but fixing that didn't help.

    Post edited by toby.allsopp on
  • surrealsurreal Posts: 168
    edited July 2022

    You have based your class on DzProperty instead of one of its subclasses.

    DzProperty::save(DzOutFile* file) is pure virtual so I assume that you have defined save(DzOutFile* file)override in your custom class???

     

    When I have created custom properties they have been associated with one or more custom Node type. The IO class for my custom node primarily did the serialising (save & load) of the node however I also used it to serialising (save & load) the associated custom property.

    Thus my IO class and the DZ_PLUGIN_REGISTER_MODIFIER_EXTRA_OBJECT_IO have been for my custom node and not specifically for the custom property.

    I don't know how you are going to get the DzNode's IO class to cast the property channel data to your custom property type, without defining a custom node class.

    With my custom nodes the node's class setPointer(---) function recognises the custom property's sectionID and casts it to the custom property class e.g. ::setPointer(...){... else if(sectionID == MYCUSTOMNODE_MYCUSTOMPROPERTYNAME_SECTION){ propertyObjectInMyCustomNode = DZ_ASSERT_CAST(ptr, MyCustomPropertyClassName);...}

    The custom node class handles the creation of the widget in which to display the property's value. All my widgets are compiled from standard QT objects, so trivial.

    As my custom properties are based on DzProperty subclasses, the save seriation is handled by the subclass e.g. if based on DzBoolProperty the data saved is "channel" : { "id" : "MyCustomPropertyName", "type" : "bool", "value" : false...  

    My custom properties don't need to save any additional data other than what is saved by the base DzProperty subclass so I don't make use of a Context* in my IO class.

    Post edited by surreal on
  • surrealsurreal Posts: 168

    I should review before I hit the submit button.

    I see a couple of points that may need clarification (ok call them inaccuracies if you wish :).

    DzProperty::save(DzOutFile* file) is not strickly "pure virtual" rather it is incomplete, if you are using DzProperty as a base class you will most likely need to override save (and many other functions) with your own e.g. setPointer and possible loadSection. So that they can handle the seriation and casting of your property.

    With my custom nodes the custom property's seriation is handled by the base class, the custom node handles the deseriation and cast to my custom property. On review this is not recommended, I should handle all property's functions within the custom property class. Another task to add to my long todo list :)

    Reading through old project comments and test results it looks like I used the custom node class to do the the deseriation because at the time I was unable to successfully develop a compound property widget and instead used the custom node class to orchestrate the on-create interaction between multiplte properties.

     

  • Hi @surreal, thanks for your help.

    I have tried overriding DzProperty::save(DzOutFile* file) and it doesn't appear to be called - my understanding was that this is for the old binary file format and is not used any more.

    I've tried inheriting from DzStringProperty instead of DzProperty and that gets me a lot further. Now the property is saved in the .duf file, but it is not loaded. I can't see any difference in the .duf file between this property and other string properties that I add to the same node withouit using my custom class, but clearly something is not quite right.

    I might have to try a custom node class at this rate, but it's not really what I want as I'd ideally like to be able to attach my custom property class to materials and possibly other things.

  • surrealsurreal Posts: 168

    You are correct, DzProperty::save(DzOutFile* file) is depreciated with v4.

    I did a bit of testing and I was wrong about setPointer as well, it and loadSection are also depreciated. The custom node is saved and loaded by IO class and DZ_PLUGIN_REGISTER_NODE_EXTRA_OBJECT_IO

    My IO classes are trivial except for createNode() which returns a pointer to a new instance of the custom class. So effectively DzExtraNodeIO does the work of recognising the custom node's data and casting it to the custom class. So I guess just need to find equivalent IO base class for property instead of node.

    Hopefully I will have some time tomorrow afternoon and I will do a bit more testing to see if I can work out how to use DzElementPropertyValueIO, DzAssetElementPropertyValueIO, DzAssetElementPropertyValuesIO to save and load custom property data.

  • Please do report back if you're able to customize IO for a custom DzProperty subclass!

    The next think I'm going to try when I get back onto this is to use the DzProperty method "setWidgetClassOverride" on a plain DzStringProperty to see if I can get the effect I want with that. The widget class override methods are not in SDK headers but are exposed for scripting so hopefully I can get at them using invokeMethod.

  • surrealsurreal Posts: 168

    Life gets in the way, as it usually does :)

    Am using DZ_PLUGIN_REGISTER_NODE_EXTRA_OBJECT_IO("My_Custom_Property", MyCustomPropertyNodeIO, DzNode); to write and read property information to/from scene file. MyCustomPropertyNodeIO is based DzExtraNodeIO class.

    Write to file in MyCustomPropertyNodeIO.writeExtraDefinition and MyCustomPropertyNodeIO.writeExtraInstance, using subclass of DzElementPropertyValueIO (and or DzAssetElementPropertyValueIO). Although have not got around to finish writing and testing that as yet.

    To read from file, will use MyCustomPropertyNodeIO.startInstanceRead and MyCustomPropertyNodeIO.applyInstanceToObject

    Not sure if that is the best way. Will know more once I have done testing with subclassed DzProperty types and compound custom properties. Most of my custom nodes require translation information however one does not, so it would be a good test candidate to be implemented as a custom property instead of as a custom node.

    Hopefully will get some time this weekend.

     

  • surrealsurreal Posts: 168
    edited July 2022

    As I said, not sure if this is the best solution and this is not fully tested, however should be enough to get you started. I may have sometime mid next week to run it through a decent test to confirm some of my issue resolutions.

    My test MyCustomStrProperty is based on DzStringProperty and does not save any double/int/float type parameters, so have not needed to implemented ReadMyCustomStrPropertyChannelParameters::addMember(const QString& name, double val). If your property has parameters of those types you will have to implement it. The QString and bool addMember(...) function will show you how.

    My choice of asset element format is modelled on channel's for better or worse.

    ///////////////////////////////////////////... EXTRACT from pluginmain.cpp//////////////////////////////////////////////////////////////////////////////////////////////////////////////////  BUG/FAULTS/ISSUE: Open/////////////////////////////////////////////////////////////////////////ISSUE 1: is there ever more than one MyCustomStrProperty on a node?//ISSUE 6: need method for making value id unique.//       MyCustomStrPropertyNodeIO::writeExtraDefinition(...) and MyCustomStrPropertyNodeIO::writeExtraInstance(...)///////////////////////////////////////////////////////////////////////////  BUG/FAULTS/ISSUE: Closed/////////////////////////////////////////////////////////////////////////Closedd ISSUE 2: should the property be copied/created on the new node instance, or is the original node's property going to manage the original and all instances of the node.//       MyCustomStrPropertyNodeIO::writeExtraInstance(...)//		 RESOLUTION 2: writeExtraInstance should not copy property from original. Property is not foundation property of DzNode. Property must be added to any instance or loaded from saved instance. //Closed ISSUE 3: should all MyCustomStrProperty property's parameters be in the extra element or should it just contain the name and the parameters in studio_node_channels? //       RESOLUTION 3: all MyCustomStrProperty property's parameters should be in the extra element. Property may contain parameters that are not recorded by studio_node_channels.  //						TODO 4: all MyCustomStrProperty property's parameters should be in the extra element.//                      TODO 5: MyCustomStrProperty property sould be excluded from studio_node_channels.//Closed ISSUE 7: should MyCustomStrProperty have a static name parameter?//		 RESOLUTION 7: as per other non-user DzProperties, to follow convention MyCustomStrProperty will have a static name parameter.//Closed ISSUE 8: should MyCustomStrProperty have a default value?//		 RESOLUTION 7: MyCustomStrProperty does not need a defaultValue.// ///////////////////////////////////////////////////////////////////////DZ_PLUGIN_CLASS_GUID(MyCustomStrProperty, 52B0198F - 98EC - 4D6F - 8F8F - 70EE82E4894E);DZ_PLUGIN_CLASS_GUID(MyCustomStrPropertyNodeIO, 2860065B - 9262 - 43BC - AD68 - 277A3FF4E389);DZ_PLUGIN_REGISTER_NODE_EXTRA_OBJECT_IO("MyCustomStr_Property_Element", MyCustomStrPropertyNodeIO, DzNode);    //DZ_PLUGIN_REGISTER_ELEMENTDATA_EXTRA_OBJECT_IO(tag, ioClass, itemClass) //itemClass has to be an Q_OBJECT e.g. MyCustomStrProperty is not an object so this fails DZ_PLUGIN_REGISTER_NODE_EXTRA_OBJECT_IO("MyCustom_Property_Element", MyCstmPropExtraElementDataIO, MyCustomStrProperty);   

     

    ///////////////////////////////////////////... EXTRACT from MyCustomStrProperty.h/////////////////////////////////////////const float MYCUSTOMPROPERTYVERSION	= 0.1f;class MyCustomStrProperty : public DzStringProperty{	friend class MyCustomStrPropertyNodeIO;	friend class MyCstmPropExtraElementDataIO;	Q_OBJECTpublic:    MyCustomStrProperty(){}    MyCustomStrProperty(const QString& name, bool isUserProperty) : DzStringProperty(name, isUserProperty){}};/// <summary>/// Class to retreive MyCustomStrProperty's parameters. /// </summary>class MyCustomStrPropertyData {public:	MyCustomStrPropertyData() {}	DzNode* parentNode = nullptr;	QString m_id = "";	QString m_type = "";	QString m_label = "";	//QString m_name = "MyCustomStrProperty";	//ISSUE 7: 	bool m_visible = true;	//QString m_defaultValue = "";	//ISSUE 8:	QString m_currentValue = "";	QString m_group = "";};//// We need applyInstanceToObject(...) however do not need createNode() so use DzAssetExtraApplyToObjectIO instead of DzExtraNodeIOclass MyCustomStrPropertyNodeIO : public DzAssetExtraApplyToObjectIO {	Q_OBJECTpublic:	MyCustomStrPropertyNodeIO() : DzAssetExtraApplyToObjectIO(), m_context(0){}	~MyCustomStrPropertyNodeIO();	virtual DzError				writeExtraDefinition(QObject* object, IDzJsonIO* io, const DzFileIOSettings* opts) const;	virtual DzError				writeExtraInstance(QObject* object, IDzJsonIO* io, const DzFileIOSettings* opts) const;	virtual DzAssetJsonObject*	startDefinitionRead(DzAssetJsonItem* parentItem);	virtual DzAssetJsonObject*	startInstanceRead(DzAssetJsonItem* parentItem);	virtual DzError				applyDefinitionToObject(QObject* object, const DzFileIOSettings* opts) const;	virtual DzError				applyInstanceToObject(QObject* object, const DzFileIOSettings* opts) const;		//DzAssetExtraApplyToObjectIO	struct						Context;	Context* m_context;};struct MyCustomStrPropertyNodeIO::Context{	Context(DzAssetFile& file) :		m_file(file)	{}	DzAssetFile& m_file;	QVector<MyCustomStrPropertyData> m_data;};class ReadMyCustomStrPropertyNode : public DzAssetJsonObject {public:	ReadMyCustomStrPropertyNode(MyCustomStrPropertyNodeIO::Context* context) :		DzAssetJsonObject(context->m_file), m_context(context)	{}	virtual DzAssetJsonItem* startMemberArray(const QString& name);	virtual bool			addMember(const QString& name, double val);	MyCustomStrPropertyNodeIO::Context* m_context;};class ReadMyCustomStrPropertyChannels : public DzAssetJsonArray {public:	ReadMyCustomStrPropertyChannels(MyCustomStrPropertyNodeIO::Context* context) :		DzAssetJsonArray(context->m_file), m_context(context)	{}	virtual DzAssetJsonItem* startObject();	MyCustomStrPropertyNodeIO::Context* m_context;};class ReadMyCustomStrPropertyChannel : public DzAssetJsonObject {public:	ReadMyCustomStrPropertyChannel(MyCustomStrPropertyNodeIO::Context* context) :		DzAssetJsonObject(context->m_file), m_context(context)	{}	virtual DzAssetJsonItem* startMemberObject(const QString& name);	virtual bool			addMember(const QString& name, const QString& val);	MyCustomStrPropertyNodeIO::Context* m_context;};class ReadMyCustomStrPropertyChannelParameters : public DzAssetJsonObject {public:	ReadMyCustomStrPropertyChannelParameters(MyCustomStrPropertyNodeIO::Context* context) :		DzAssetJsonObject(context->m_file), m_context(context)	{}	virtual bool			addMember(const QString& name, const QString& val);	//virtual bool			addMember(const QString& name, double val);	//Not storing float or double parameters in MyCustomStrProperty so not required.	virtual bool			addMember(const QString& name, bool val);	MyCustomStrPropertyNodeIO::Context* m_context;};
    ///////////////////////////////////////////... EXTRACT from MyCustomStrProperty.cpp////////////////////////////////////////////////////////////////////////////////////////////////////////////////// MyCustomStrPropertyNodeIO///////////////////////////////////////////////////////////////////////MyCustomStrPropertyNodeIO::~MyCustomStrPropertyNodeIO() { 	delete m_context;}//ISSUE 1: is there ever more than one MyCustomStrProperty on a node?//ISSUE 3: should all the properties parameters be in the extra element or should it just contain the name and the parameters in studio_node_channels?//ISSUE 7: should MyCustomStrProperty have a static name parameter? As per other non-user DzProperties, to follow convention MyCustomStrProperty will have a static name parameter.//ISSUE 8: should MyCustomStrProperty have a default value? MyCustomStrProperty does not need a defaultValue. //This function handles what happens when a node is saved. DzError MyCustomStrPropertyNodeIO::writeExtraDefinition(QObject* object, IDzJsonIO* io, const DzFileIOSettings* opts)const{	DzNode* node = qobject_cast<DzNode*>(object);	if (node) {		int numP = node->getNumProperties();		io->addMember("version", MYCUSTOMPROPERTYVERSION);		io->startMemberArray("channels");	//ISSUE 1: Should this be an array? Is there ever more than one MyCustomStrProperty on a node?		for (int i = 0; i < numP; i++) {			if (node->getProperty(i)->inherits("MyCustomStrProperty")) {				MyCustomStrProperty* p = qobject_cast<MyCustomStrProperty*>(node->getProperty(i));				if (p) {					io->startObject();					io->startMemberObject("channel");					io->addMember("id", p->getName()); //ISSUE 6: need method for making value id unique.					io->addMember("type", "MyCustomStrProperty");					io->addMember("label", p->getLabel());					//io->addMember("name", p->getName()); //ISSUE 7: as per other non-user DzProperties, to follow convention MyCustomStrProperty will have a static name parameter.					if (p->isHidden())						io->addMember("visible", false);					//io->addMemberNull("value"); //ISSUE 8: should MyCustomStrProperty have a default value? MyCustomStrProperty does not need a defaultValue. If it did then io->addMember("value", somevalue) as DzStringProperty does not have a default.					io->addMember("current_value", p->getValue());					io->finishObject();	//finish channel					io->addMember("group", p->getGroup()->getPath());					io->finishObject();				}			}		}		io->finishArray();	}	return DZ_NO_ERROR;// DzAssetExtraObjectIO::writeExtraDefinition(object, io, opts);}//ISSUE 2: should the property be copied/created on the new node instance, or is the original node's property going to manage the original and all instances of the node.//This function handles what happens when a new instance of the node is saved. DzError MyCustomStrPropertyNodeIO::writeExtraInstance(QObject* object, IDzJsonIO* io, const DzFileIOSettings* opts)const{	return DZ_NO_ERROR;// DzAssetExtraObjectIO::writeExtraInstance(object, io, opts);}//This function handles what happens when a node is read from fileDzAssetJsonObject* MyCustomStrPropertyNodeIO::startDefinitionRead(DzAssetJsonItem* parentItem){	delete m_context;	m_context = new Context(parentItem->getFile());	return new ReadMyCustomStrPropertyNode(m_context);}//This function handles what happens when a node instance is readDzAssetJsonObject* MyCustomStrPropertyNodeIO::startInstanceRead(DzAssetJsonItem* parentItem){	return nullptr;// DzAssetExtraObjectIO::startInstanceRead(parentItem);}//This function handles applying the asset json object i.e. creation of the property from file data and applying it to the node.  DzError MyCustomStrPropertyNodeIO::applyDefinitionToObject(QObject* object, const DzFileIOSettings* opts)const{	DzNode* node = dynamic_cast<DzNode*>(object);	if (node) {		for (int i = 0; i < m_context->m_data.size(); i++)		{			if (!m_context->m_data[i].m_type.compare("MyCustomStrProperty")) {				MyCustomStrProperty* p = new MyCustomStrProperty(m_context->m_data[i].m_id, false);				//p->setName(m_context->m_data[i].m_id);				p->setLabel(m_context->m_data[i].m_label);				p->setHidden(!m_context->m_data[i].m_visible);				//p->setDefaultValue(m_context->m_data[i].m_defaultValue);	//ISSUE 8:				p->setValue(m_context->m_data[i].m_currentValue);				DzError e = node->addProperty(p);				if (DZ_NO_ERROR != e) {					return e;				}			}		}	}	return DZ_NO_ERROR;//  DzAssetExtraObjectIO::applyDefinitionToObject(object, opts);}//This function handles applying the asset (instance json object i.e. creation of the property from file data and applying it to the node instance.  DzError MyCustomStrPropertyNodeIO::applyInstanceToObject(QObject* object, const DzFileIOSettings* opts)const{	return DZ_NO_ERROR;}///////////////////////////////////////////////////////////////////////// ReadMyCustomStrPropertyNode///////////////////////////////////////////////////////////////////////DzAssetJsonItem* ReadMyCustomStrPropertyNode::startMemberArray(const QString& name){	if (name == "channels")	{		return new ReadMyCustomStrPropertyChannels(m_context);	}	return DzAssetJsonObject::startMemberArray(name);}bool ReadMyCustomStrPropertyNode::addMember(const QString& name, double val){	if (name == "version")	{		if (val > 0.0) {			return true;		}	}	return DzAssetJsonObject::addMember(name, val);}///////////////////////////////////////////////////////////////////////// ReadMyCustomStrPropertyChannels///////////////////////////////////////////////////////////////////////DzAssetJsonItem* ReadMyCustomStrPropertyChannels::startObject(){	return new ReadMyCustomStrPropertyChannel(m_context);}///////////////////////////////////////////////////////////////////////// ReadMyCustomStrPropertyChannel///////////////////////////////////////////////////////////////////////DzAssetJsonItem* ReadMyCustomStrPropertyChannel::startMemberObject(const QString& name){	if (name == "channel")	{		m_context->m_data.push_back(MyCustomStrPropertyData());		return new ReadMyCustomStrPropertyChannelParameters(m_context);	}	return DzAssetJsonObject::startMemberArray(name);}bool ReadMyCustomStrPropertyChannel::addMember(const QString& name, const QString& val){	const int index = m_context->m_data.size() - 1;	if (name == "group") {		m_context->m_data[index].m_group = val;;	}	return true;}///////////////////////////////////////////////////////////////////////// ReadMyCustomStrPropertyChannelParameters///////////////////////////////////////////////////////////////////////bool ReadMyCustomStrPropertyChannelParameters::addMember(const QString& name, const QString& val){	const int index = m_context->m_data.size() - 1;	if (name == "id")	{		m_context->m_data[index].m_id = val;		return true;	}	else if (name == "type")	{		m_context->m_data[index].m_type = val;		return true;	}	else if (name == "label")	{		m_context->m_data[index].m_label = val;		return true;	}	//else if (name == "name")	//ISSUE 7:	//{	//	m_context->m_data[index].m_name = val;	//}	//else if (name == "value")	//ISSUE 8:	//{	//	m_context->m_data[index].m_value = val;	//	return true;	//}	else if (name == "current_value")	{		m_context->m_data[index].m_currentValue = val;		return true;	}	return DzAssetJsonObject::addMember(name, val);}//Not storing float or double parameters in MyCustomStrProperty so not required./*bool ReadMyCustomStrPropertyChannelParameters::addMember(const QString& name, double val){	return DzAssetJsonObject::addMember(name, val);}*/bool ReadMyCustomStrPropertyChannelParameters::addMember(const QString& name, bool val){	const int index = m_context->m_data.size() - 1;	if (name == "visible")	{		m_context->m_data[index].m_visible = val;		return true;	}	return DzAssetJsonObject::addMember(name, val);}

    example of the asset file data

    ...
    "extra" : [
        {        
            "type" : "studio/node/MyCustomStr_Property_Element",
            "version" : 0.1,
            "channels" : [
                {
                    "channel" : {
                        "id" : "my_test_str",
                        "type" : "MyCustomStrProperty",
                        "label" : "my_test_str",
                        "current_value" : "testtest_y"
                    },
                    "group" : "/General"
                }
            ]
        },
        {
            "type" : "studio_node_channels",
    ...

    Post edited by surreal on
  • surrealsurreal Posts: 168

    Oops! The MyCustomStrPropertyNodeIO destructor needs to be in .cpp instead of the .h. Have edited the above code snipets.

  • Ah, interesting, thank you @surreal! I would not have guessed that it was allowed to register an IO class for DzNode, but I guess that's why it has "extra" in the name.

    Do you have any ideas for how to resolve your TODO 5 - i.e. how to remove the custom property from the default serialization of properties? Maybe it doesn't really matter too much?

  • surrealsurreal Posts: 168

    TODO 4&5 was on my mind all Sunday so burnt a bit of the midnight oil last night.

    After some testing I think my choice to use Node instead of Property was the correct choice.

    1. If an object has a single (String, Bool, Int, Float) presentation then use DzModifier (not Dz*Property).
       
    2. If an object has multiple or custom presentation manipulator(s) then use DzNode. Unused default properties (i.e. Translation) should be set to hidden. The SDK/Samples/Saving/CustomElements covers custom DzNode and custom DzModifier.
       

    I have updated the code snipet above, with latest findings before I terminated my testing.

    My TODO list is a bit longer with a few updates I should do to my old custom nodes. The oldest source file has a 2012 creation date so they are overdue for a review. Code reuse has its advantages but also has its pit falls. We all need a bit of a refresher now and again :)

    Best of luck with your project.

Sign In or Register to comment.