Limit SplineKnot tangent

I want to limit the maximum and minimum values of the tangent of SplineKnot so that it does not exceed the maximum and minimum values of SplineKnot. What should I do?

From
9556934c-5ddb-4d0a-b5c7-68b9190a00ff-image.png
to
d755faf0-09ea-43e0-beab-2891b4c08739-image.png

Thank,
AiMiDi

Hi @AiMiDi,

thank you for reaching out to us.

In principle your question of clamping a parameter is answered with overwriting NodeData::Message. Where one then listens for MSG_DESCRIPTION_POSTSETPARAMETER to modify the just set value according to ones needs and then writes it back. This works fine for atomic data types like DTYPE_REAL, but there is a problem when approaching it like that with more complex data types with more complex GUIs, e.g, SplineData.

The problem is that one cannot access an instance of a parameter, but only a copy of it with NodeData::GetParameter, which in turn forces one to write back the modified data. For SplineData this means that this will flush the current vertex selection state of the gadget, making this approach unviable, as it will render the user incapable of selecting a vertex. This also applies to the case of MSG_DESCRIPTION_USERINTERACTION_START and MSG_DESCRIPTION_USERINTERACTION_END, resulting in the same problem of making a vertex selection in the SplineData will immediately flush it.

An alternative route is to use SplineData::SetUserCallback to establish a callback with which to intercept changes made to the SplineData via SPLINE_CALLBACK_CORE_MESSAGE. The problem here is that this will work like MSG_DESCRIPTION_USERINTERACTION_END, i.e., it is only going to be raised once the user does release the mouse - ends the interaction. And not while dragging a value like MSG_DESCRIPTION_POSTSETPARAMETER is being raised for. I currently do not see a more elegant way to do this. You will find an example for that at the end of the posting.

There is also the problem of a slight ambiguity in your question, since:

I want to limit the maximum and minimum values of the tangent of SplineKnot so that it does not exceed the maximum and minimum values of SplineKnot.

could be interpreted in multiple ways. I went in my solution for the simplest interpretation of projecting the spline handles orthogonally back onto the [0, 1] interval box in the GUI, if you want this to be fancier, you will have to do the line-line intersection calculation (between a border of the box and your tangent vector) yourself.

I hope this helps and cheers,
Ferdinand

The code (for an ObjectData example):

// For https://plugincafe.maxon.net/topic/13277

#include "c4d.h"
#include "c4d_symbols.h"
#include "obase.h"
#include "maxon/apibase.h"
#include "c4d_objectdata.h"
#include "c4d_basedocument.h"
#include "lib_description.h"
#include "customgui_splinecontrol.h"

// The callback function which does clamp the SplineData
static bool SplineDataCallBack(Int32 cid, const void* data)
{
	// There are other callback types we are just using SPLINE_CALLBACK_CORE_MESSAGE here.
	if (cid == SPLINE_CALLBACK_CORE_MESSAGE)
	{
		if (data == nullptr)
			return true;

		// Some data massaging ...
		SplineDataCallbackCoreMessage* coreMessageData = (SplineDataCallbackCoreMessage*) data;
		SplineCustomGui* splineCustomGui = coreMessageData->pGUI;
		SplineData* splineData = splineCustomGui->GetSplineData();
		if (splineData == nullptr)
			return true;
		
		// Now we just go over all knots and clamp them. This could also be done more elegantly
		// with line-line intersections, I am just clamping each tangent here to the interval [0, 1],
		// which effectively equates orthogonally projecting the tangents onto that "[0,  1] box". If you
		// want to scale vectors so that a handle which is outside that [0, 1] box, is scaled in such
		// manner that the handle touches the border of that box, you will have to use line-line 
		// intersections.
		for (int i = 0; i < splineData->GetKnotCount(); i++)
		{
			// Get the current knot.
			CustomSplineKnot* knot = splineData->GetKnot(i);
			// Tangents live in a vector space relative to their vertex, so we make the tangent vectors
			// global first and then clamp them.
			Vector globalLeftTangent = (knot->vPos + knot->vTangentLeft).Clamp01();
			Vector globalRightTangent = (knot->vPos + knot->vTangentRight).Clamp01();
			// And then we convert the tangents back to their local tangent space and write them back.
			knot->vTangentLeft = globalLeftTangent - knot->vPos;
			knot->vTangentRight = globalRightTangent - knot->vPos;
		}
 	}
	return true;
}

class Pc13277 : public ObjectData
{
	INSTANCEOF(Pc13277, ObjectData)

public:
	static NodeData* Alloc() { return NewObjClear(Pc13277); }

	// We bind the callback on the initialization of the node.
	virtual Bool Init(GeListNode* node)
	{
		if (node == nullptr || !SUPER::Init(node))
			return false;
		BaseContainer* bc = ((BaseList2D*)node)->GetDataInstance();

		// Create a SplineData instance and set the callback function.
		GeData geData(CUSTOMDATATYPE_SPLINE, DEFAULTVALUE);
		SplineData* splineData = (SplineData*)geData.GetCustomDataType(CUSTOMDATATYPE_SPLINE);
		splineData->SetUserCallback(SplineDataCallBack, nullptr);

		// Populate the SplineData as usual ...
		if (splineData)
			splineData->MakeLinearSplineBezier(2);
		bc->SetData(ID_SPLINEDATA, geData);

		return true;
	}

	// And when something replaces our spline data instance.
	virtual bool SetDParameter(GeListNode* node, const DescID& id, const GeData& t_data, DESCFLAGS_SET& flags) 
	{
		if (id[0].id == ID_SPLINEDATA) 
		{
			SplineData* splineData = (SplineData*)t_data.GetCustomDataType(CUSTOMDATATYPE_SPLINE);
			splineData->SetUserCallback(SplineDataCallBack, nullptr);
		}
		return SUPER::SetDParameter(node, id, t_data, flags);
	}
};

How it will work:
456.gif

Thanks for your answer, it solved my problem.
I used: Vector::ClampMax() and Vector::ClampMin() to limit the tangent to the value I want.

Thank,
AiMiDi