Solved Undo wanted - TransferGoal() and nested instances

Hi there,

I came across a little problem about cloning nested instances and TransferGoal().
Let my try to describe the problem. I have the following setup:

0_1538768755462_7c4af3fa-7837-4e0b-8efe-242b06707ff3-image.png

"nested instance" is pointing to "direct instance" and this in turn pointing to the reference object.
Now I am trying to make the instances "editable" - quotation marks because it's just relinking & cloning linked objects which has the same effect in the end and saves a bit time and a few lines of code.

My approach is as follows: Get the linked object and replace it with the instance object currently processed. So to speak: Clone the reference, insert it and remove the original instance object. This is because it also works with deeply nested instance hierarchies.

This all works quite well, but there is an annoying problem that I cannot get rid of. When calling undo, the nested instance loses its link. I tried several approaches like using AliasTrans, adding undos in the helper routines, etc. I really have no clue, what is going on. Maybe it is a bug?

Here's my actual code, so I didn't condense it. Hope, it doesn't matter.

From Command_MakeEditable.h (CommandData Plugin)

Bool Execute(BaseDocument* doc) override
	{
		if (!doc)
			return false;

		// Detect Key modifiers
		BaseContainer state;
		GetInputState(BFM_INPUT_MOUSE, BFM_INPUT_MOUSELEFT, state);
		const auto bCtrl = (state.GetInt32(BFM_INPUT_QUALIFIER) & QCTRL) != 0;

		doc->StartUndo();

		// Create Array that holds all objects to operate on
		const AutoAlloc<AtomArray> activeObjects;
		doc->GetActiveObjects(*activeObjects, GETACTIVEOBJECTFLAGS::CHILDREN);

		// empty? quit.
		if (!activeObjects)
			return false;

		for (auto i = 0; i < activeObjects->GetCount(); ++i)
		{
			auto obj = static_cast<BaseObject*>(activeObjects->GetIndex(i));
			if (!obj)
				continue;

			// Make editable magic
			if (obj->IsInstanceOf(Oinstance))
			{
				// Convert a single instance
				auto convertedInstance = g_MakeInstanceEditable(doc, obj, bCtrl);
				if (convertedInstance == nullptr) // Something went wrong, skip
					continue;

				// Insert it into the document
				doc->InsertObject(convertedInstance, obj->GetUp(), obj->GetPred());
				doc->AddUndo(UNDOTYPE::NEWOBJ, convertedInstance);

				// Select the new object
				doc->AddUndo(UNDOTYPE::BITS, convertedInstance);
				convertedInstance->SetBit(BIT_ACTIVE);

				// Update links
				doc->AddUndo(UNDOTYPE::CHANGE, convertedInstance);
				obj->TransferGoal(convertedInstance, false);

				// Remove the original instance object to finally replace it with the converted one
				doc->AddUndo(UNDOTYPE::DELETEOBJ, obj);
				obj->Remove();
				BaseObject::Free(obj);
			}
		}

		doc->EndUndo();
		EventAdd();

		return true;
	}

The helper functions:

inline BaseObject* g_MakeInstanceEditable(BaseDocument* doc, BaseObject* obj, const bool deep = false)
{
	if (obj == nullptr || !doc)
		return nullptr;

	if (obj->IsInstanceOf(Oinstance))
	{
		// Retrieve the linked or root object
		// obj is instance and root is requested, so simply return a copy of the root object
		auto refObj = g_GetInstanceRef(obj, deep);
		if (!refObj)
			return nullptr;

		return static_cast<BaseObject*>(refObj->GetClone(COPYFLAGS::NONE, nullptr));
	}
	return nullptr;
}
// Get shallow or deeply linked reference objects
/**
 * @brief Retrieve the direct or root linked object of an instance. Also works with nested instances
 * @param obj the instance object
 * @param deep if true, the root object of nested instances is returned
 * @return the reference or root object. Can return nullptr.
 */
inline BaseObject* g_GetInstanceRef(BaseObject* obj, const Bool deep = false)
{
	if (!obj->IsInstanceOf(Oinstance))
		return nullptr;

	// Retrieve Link from instance
	GeData data;
	if (!obj->GetParameter(DescID(INSTANCEOBJECT_LINK), data, DESCFLAGS_GET::NONE))
		return nullptr;

	// The document needs to be provided for GetLinkAtom()
	const auto doc = obj->GetDocument();
	if (!doc)
		return nullptr;

	// Get the Atom
	auto linkedEntity = static_cast<BaseObject*>(data.GetLinkAtom(doc, Obase));

	// Get down to the reference object if root object is requested
	while (deep && linkedEntity->IsInstanceOf(Oinstance))
	{
		if (!linkedEntity->GetParameter(DescID(INSTANCEOBJECT_LINK), data, DESCFLAGS_GET::NONE))
			return nullptr;

		linkedEntity = static_cast<BaseObject*>(data.GetLinkAtom(doc, Obase));
	}

	return linkedEntity;
}

Any help here would be highly appreciated!

Thanks in advance,
Robert

Hi @mp5gosu, sorry for the delay it took me a while to figure it out this topic.

The first issue is that you are adding an Undo to an object which will be deleted.
So if we get an object called "A", an Instance called "1" pointing to "A" and another instance called "2" pointing to "1", and in the object manager they are ordered as follow A => 1 => 2 and you select all tree and run your script.
In the First iteration, A is passed.
On the second iteration, 1 is converted to a copy of A, "Copy A" is inserted and goal is transferred and then the first issue, you make undo only for the instance "1" while the actual parameter which is changing is in the link in instance "2". Then you delete "1".
So at the end of the first iteration, we get "A", "Copy A" and "2" which is pointing to "Copy A".
On the third iteration, "Copy A "is again copied to "Copy Copy A", you insert this "Copy Copy A", you transfer goal, here, in fact, we don't need any undo since nothing is referring to this object in the scene. And we delete this object.

Now if we press undo. The whole process happens reversely.
So Instance object "2" is recreated, and point to "Copy A" then "Copy Copy A" is deleted. Then Instance Object "1" is recreated and point to "A", but due to improper undo handling of TransferGoal I explained before, TrasnferGoal is not called so "2" still point to "Copy A" which will be then Deleted.

So in order to fix everythings here is a modified version of g_MakeInstanceEditable

inline BaseObject* g_MakeInstanceEditable(BaseDocument* doc, BaseObject* obj, const bool deep = false)
{
	if (obj == nullptr || !doc)
		return nullptr;

	BaseObject* refObj = obj;
	doc->AddUndo(UNDOTYPE::CHANGE, refObj); // We are sure we will change the Goal of this object.
	while (refObj->IsInstanceOf(Oinstance)) // Loop until we get the final obj and not an instance
	{
		// Retrieve the linked or root object
		// obj is instance and root is requested, so simply return a copy of the root object
		refObj = g_GetInstanceRef(refObj, deep);
		if (!refObj)
			return nullptr;
	}

	return static_cast<BaseObject*>(refObj->GetClone(COPYFLAGS::NONE, nullptr));
}

and the main function.

if (!doc)
	return false;

// Detect Key modifiers
BaseContainer state;
GetInputState(BFM_INPUT_MOUSE, BFM_INPUT_MOUSELEFT, state);
const auto bCtrl = (state.GetInt32(BFM_INPUT_QUALIFIER) & QCTRL) != 0;

doc->StartUndo();

// Create Array that holds all objects to operate on
const AutoAlloc<AtomArray> activeObjects;
doc->GetActiveObjects(*activeObjects, GETACTIVEOBJECTFLAGS::CHILDREN);

// empty? quit.
if (!activeObjects)
	return false;

for (auto i = 0; i < activeObjects->GetCount(); ++i)
{
	auto obj = static_cast<BaseObject*>(activeObjects->GetIndex(i));
	if (!obj)
		continue;

	// Make editable magic
	if (obj->IsInstanceOf(Oinstance))
	{
		// Convert a single instance
		auto convertedInstance = g_MakeInstanceEditable(doc, obj, bCtrl);
		if (convertedInstance == nullptr) // Something went wrong, skip
			continue;

		// Insert it into the document
		doc->InsertObject(convertedInstance, obj->GetUp(), obj->GetPred());
		doc->AddUndo(UNDOTYPE::NEWOBJ, convertedInstance);

		// Select the new object
		doc->AddUndo(UNDOTYPE::BITS, convertedInstance);
		convertedInstance->SetBit(BIT_ACTIVE);

		//Undo handle in g_MakeInstanceEditable
		obj->TransferGoal(convertedInstance, false);

		// Remove the original instance object to finally replace it with the converted one
		doc->AddUndo(UNDOTYPE::DELETEOBJ, obj);
		obj->Remove();
		BaseObject::Free(obj);
	}
}

doc->EndUndo();
EventAdd();

return true;

If you have any questions, please let me know.
Cheers,
Maxime!

Hey Maxime,

thank you very much for the detailed explanation. Makes absolute sense now! It works perfectly now. :)
I tried a similar approach, but I was adding the Undo in g_MakeInstanceEditable() per object - that was the main culprit.

Cheers,
Robert