SOLVED GetContour() and return Onull

Hello, I had a question about something in the C++ SDK underneath GetContour() it says:

GetContour()...


Returns
    The spline contour.
    If the generator does not produce any output (e.g. when the user chose wrong settings) it must at least return an (empty) Onull object, otherwise Cinema 4D will try to rebuild the cache again and again.
    Only return nullptr in the case of a memory error.

It seems like if you had a case where you would have invalid settings you would return an Onull as opposed to a spline. When I go to return an Onull in this circumstance though I have a crash. I did a quick test in the Circle example and this crashes.

SplineObject* DoubleCircleData::GetContour(BaseObject* op, BaseDocument* doc, Float lod, BaseThread* bt)
{
    BaseContainer* bc = op->GetDataInstance();
    if (!bc)
        return nullptr;
    SplineObject* bp = GenerateCircle(bc->GetFloat(CIRCLEOBJECT_RAD));
    if(bc->GetFloat(CIRCLEOBJECT_RAD) >500.0) // Added to test the case
    {
        return static_cast<SplineObject*>(BaseObject::Alloc(Onull));
    }
    if (!bp)
        return nullptr;
    BaseContainer* bb = bp->GetDataInstance();
    
    bb->SetInt32(SPLINEOBJECT_INTERPOLATION, bc->GetInt32(SPLINEOBJECT_INTERPOLATION));
    bb->SetInt32(SPLINEOBJECT_SUB, bc->GetInt32(SPLINEOBJECT_SUB));
    bb->SetFloat(SPLINEOBJECT_ANGLE, bc->GetFloat(SPLINEOBJECT_ANGLE));
    bb->SetFloat(SPLINEOBJECT_MAXIMUMLENGTH, bc->GetFloat(SPLINEOBJECT_MAXIMUMLENGTH));
    
    OrientObject(bp, bc->GetInt32(PRIM_PLANE), bc->GetBool(PRIM_REVERSE));
    
    return bp;
}

Am I misunderstanding something here? I thought the GetContour entry was pretty clear but I seem to be doing it wrong.

Thanks for any info,
Dan

Hi @d_schmidt,

thank you for reaching out to us. And thank you for pointing out this false information in the C++ SDK documentation. In fact you cannot return a BaseObject in ObjectData::GetContour, instead you should return an empty spline. I added a modified double circle example at the end to demonstrate that approach.

About your code, there are also two other things that stand out to me. I was actually surprised that static_cast<SplineObject*>(BaseObject::Alloc(Onull)) did compile for you, because there is no direct implicit or explicit conversion from BaseObject to SplineObject. @m_adam then pointed out that this probably does compile because you can convert via BaseList2D and the type symbols to a SplineObject. But this cast is probably still the reason why Cinema 4D does crash for you, since this casting probably produces garbage. There is also the fact that you allocate bp and leave it behind in the case when CIRCLEOBJECT_RAD > 500.0.

I have added the documentation issue to our task list and it will be reflected in an upcoming release.

I hope this helps and cheers,
Ferdinand

The relevant bits:

static SplineObject* GenerateCircle(Float rad)
{
#define TANG 0.415
// Our little radius cutoff value.
#define MAX_RADIUS 500.0

    // If the passed radius exceeds our MAX_RADIUS, bail and return an
    // empty spline.
    if (rad > MAX_RADIUS) {
        SplineObject* dummy = SplineObject::Alloc(0, SPLINETYPE::LINEAR);
        dummy->Message(MSG_UPDATE);
        return dummy;
    }
    ...

SplineObject* DoubleCircleData::GetContour(BaseObject* op, BaseDocument* doc, Float lod, BaseThread* bt)
{
    BaseContainer* bc = op->GetDataInstance();
    if (!bc)
        return nullptr;
    // This will now return an empty spline when CIRCLEOBJECT_RAD > MAX_RADIUS
    SplineObject* bp = GenerateCircle(bc->GetFloat(CIRCLEOBJECT_RAD));
    ...

The whole file:

// spline example

#include "c4d.h"
#include "c4d_symbols.h"
#include "odoublecircle.h"
#include "main.h"

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

public:
	virtual Bool Init(GeListNode* node);

	virtual Bool Message				(GeListNode* node, Int32 type, void* data);
	virtual Int32 GetHandleCount(BaseObject* op);
	virtual void GetHandle(BaseObject* op, Int32 i, HandleInfo& info);
	virtual void SetHandle(BaseObject* op, Int32 i, Vector p, const HandleInfo& info);
	virtual SplineObject* GetContour(BaseObject* op, BaseDocument* doc, Float lod, BaseThread* bt);
	virtual Bool GetDEnabling(GeListNode* node, const DescID& id, const GeData& t_data, DESCFLAGS_ENABLE flags, const BaseContainer* itemdesc);

	static NodeData* Alloc() { return NewObjClear(DoubleCircleData); }
};

Bool DoubleCircleData::Message(GeListNode* node, Int32 type, void* data)
{
	if (type == MSG_MENUPREPARE)
	{
		BaseDocument* doc = (BaseDocument*)data;
		((BaseObject*)node)->GetDataInstance()->SetInt32(PRIM_PLANE, doc->GetSplinePlane());
	}

	return true;
}

Bool DoubleCircleData::Init(GeListNode* node)
{
	BaseObject*		 op = (BaseObject*)node;
	BaseContainer* data = op->GetDataInstance();
	if (!data)
		return false;

	data->SetFloat(CIRCLEOBJECT_RAD, 200.0);
	data->SetInt32(PRIM_PLANE, 0);
	data->SetBool(PRIM_REVERSE, false);
	data->SetInt32(SPLINEOBJECT_INTERPOLATION, SPLINEOBJECT_INTERPOLATION_ADAPTIVE);
	data->SetInt32(SPLINEOBJECT_SUB, 8);
	data->SetFloat(SPLINEOBJECT_ANGLE, DegToRad(5.0));
	data->SetFloat(SPLINEOBJECT_MAXIMUMLENGTH, 5.0);

	return true;
}

static Vector SwapPoint(const Vector& p, Int32 plane)
{
	switch (plane)
	{
		case 1: return Vector(-p.z, p.y, p.x); break;
		case 2: return Vector(p.x, -p.z, p.y); break;
	}
	return p;
}

Int32 DoubleCircleData::GetHandleCount(BaseObject* op)
{
	return 1;
}
void DoubleCircleData::GetHandle(BaseObject* op, Int32 i, HandleInfo& info)
{
	BaseContainer* data = op->GetDataInstance();
	if (!data)
		return;

	Float rad = data->GetFloat(CIRCLEOBJECT_RAD);
	Int32 plane = data->GetInt32(PRIM_PLANE);

	info.position	 = SwapPoint(Vector(rad, 0.0, 0.0), plane);
	info.direction = !SwapPoint(Vector(1.0, 0.0, 0.0), plane);
	info.type = HANDLECONSTRAINTTYPE::LINEAR;
}

void DoubleCircleData::SetHandle(BaseObject* op, Int32 i, Vector p, const HandleInfo& info)
{
	BaseContainer* data = op->GetDataInstance();
	if (!data)
		return;

	Float val = Dot(p, info.direction);

	data->SetFloat(CIRCLEOBJECT_RAD, ClampValue(val, 0.0_f, (Float) MAXRANGE));
}

// --- Modifications start here. ---

static SplineObject* GenerateCircle(Float rad)
{
#define TANG 0.415
// Our little radius cutoff value.
#define MAX_RADIUS 500.0

    // If the passed radius exceeds our MAX_RADIUS, bail and return an
    // empty spline.
	if (rad > MAX_RADIUS) {
		SplineObject* dummy = SplineObject::Alloc(0, SPLINETYPE::LINEAR);
		dummy->Message(MSG_UPDATE);
		return dummy;
	}
    // --- Modifications end here ---

	Float	sn, cs;
	Int32	i, sub = 4;

	SplineObject* op = SplineObject::Alloc(sub * 2, SPLINETYPE::BEZIER);

	if (!op || !op->MakeVariableTag(Tsegment, 2))
	{
		blDelete(op); return nullptr;
	}
	op->GetDataInstance()->SetBool(SPLINEOBJECT_CLOSED, true);

	Vector*	 padr = op->GetPointW();
	Tangent* hadr = op->GetTangentW();
	Segment* sadr = op->GetSegmentW();

	if (sadr)
	{
		sadr[0].closed = true;
		sadr[0].cnt = sub;
		sadr[1].closed = true;
		sadr[1].cnt = sub;
	}

	if (hadr && padr)
	{
		for (i = 0; i < sub; i++)
		{
			SinCos(2.0 * PI * i / Float(sub), sn, cs);

			padr[i] = Vector(cs * rad, sn * rad, 0.0);
			hadr[i].vl = Vector(sn * rad * TANG, -cs * rad * TANG, 0.0);
			hadr[i].vr = -hadr[i].vl;

			padr[i + sub] = Vector(cs * rad, sn * rad, 0.0) * 0.5;
			hadr[i + sub].vl = Vector(sn * rad * TANG, -cs * rad * TANG, 0.0) * 0.5;
			hadr[i + sub].vr = -hadr[i + sub].vl;
		}
	}

	op->Message(MSG_UPDATE);

	return op;
}

static void OrientObject(SplineObject* op, Int32 plane, Bool reverse)
{
	Vector*	 padr = ToPoint(op)->GetPointW();
	Tangent* hadr = ToSpline(op)->GetTangentW(), h;
	Int32		 pcnt = ToPoint(op)->GetPointCount(), i;

	if (!hadr && ToSpline(op)->GetTangentCount())
		return;
	if (!padr && ToPoint(op)->GetPointCount())
		return;

	if (plane >= 1)
	{
		switch (plane)
		{
			case 1:	// ZY
				for (i = 0; i < pcnt; i++)
				{
					padr[i] = Vector(-padr[i].z, padr[i].y, padr[i].x);
					if (!hadr)
						continue;
					hadr[i].vl = Vector(-hadr[i].vl.z, hadr[i].vl.y, hadr[i].vl.x);
					hadr[i].vr = Vector(-hadr[i].vr.z, hadr[i].vr.y, hadr[i].vr.x);
				}
				break;

			case 2:	// XZ
				for (i = 0; i < pcnt; i++)
				{
					padr[i] = Vector(padr[i].x, -padr[i].z, padr[i].y);
					if (!hadr)
						continue;
					hadr[i].vl = Vector(hadr[i].vl.x, -hadr[i].vl.z, hadr[i].vl.y);
					hadr[i].vr = Vector(hadr[i].vr.x, -hadr[i].vr.z, hadr[i].vr.y);
				}
				break;
		}
	}

	if (reverse)
	{
		Vector p;
		Int32	 to = pcnt / 2;
		if (pcnt % 2)
			to++;
		for (i = 0; i < to; i++)
		{
			p = padr[i]; padr[i] = padr[pcnt - 1 - i]; padr[pcnt - 1 - i] = p;
			if (!hadr)
				continue;
			h = hadr[i];
			hadr[i].vl = hadr[pcnt - 1 - i].vr;
			hadr[i].vr = hadr[pcnt - 1 - i].vl;
			hadr[pcnt - 1 - i].vl = h.vr;
			hadr[pcnt - 1 - i].vr = h.vl;
		}
	}
	op->Message(MSG_UPDATE);
}

SplineObject* DoubleCircleData::GetContour(BaseObject* op, BaseDocument* doc, Float lod, BaseThread* bt)
{
	BaseContainer* bc = op->GetDataInstance();
	if (!bc)
		return nullptr;
    // This will now return an empty spline when CIRCLEOBJECT_RAD > MAX_RADIUS
	SplineObject* bp = GenerateCircle(bc->GetFloat(CIRCLEOBJECT_RAD));
	if (!bp)
		return nullptr;
	BaseContainer* bb = bp->GetDataInstance();

	bb->SetInt32(SPLINEOBJECT_INTERPOLATION, bc->GetInt32(SPLINEOBJECT_INTERPOLATION));
	bb->SetInt32(SPLINEOBJECT_SUB, bc->GetInt32(SPLINEOBJECT_SUB));
	bb->SetFloat(SPLINEOBJECT_ANGLE, bc->GetFloat(SPLINEOBJECT_ANGLE));
	bb->SetFloat(SPLINEOBJECT_MAXIMUMLENGTH, bc->GetFloat(SPLINEOBJECT_MAXIMUMLENGTH));

	OrientObject(bp, bc->GetInt32(PRIM_PLANE), bc->GetBool(PRIM_REVERSE));

	return bp;
}

Bool DoubleCircleData::GetDEnabling(GeListNode* node, const DescID& id, const GeData& t_data, DESCFLAGS_ENABLE flags, const BaseContainer* itemdesc)
{
	Int32 inter;
	BaseContainer* data = ((BaseObject*)node)->GetDataInstance();
	if (!data)
		return false;

	switch (id[0].id)
	{
		case SPLINEOBJECT_SUB:
			inter = data->GetInt32(SPLINEOBJECT_INTERPOLATION);
			return inter == SPLINEOBJECT_INTERPOLATION_NATURAL || inter == SPLINEOBJECT_INTERPOLATION_UNIFORM;

		case SPLINEOBJECT_ANGLE:
			inter = data->GetInt32(SPLINEOBJECT_INTERPOLATION);
			return inter == SPLINEOBJECT_INTERPOLATION_ADAPTIVE || inter == SPLINEOBJECT_INTERPOLATION_SUBDIV;

		case SPLINEOBJECT_MAXIMUMLENGTH:
			return data->GetInt32(SPLINEOBJECT_INTERPOLATION) == SPLINEOBJECT_INTERPOLATION_SUBDIV;
	}
	return true;
}

// be sure to use a unique ID obtained from www.plugincafe.com
#define ID_CIRCLEOBJECT 1001154

Bool RegisterCircle()
{
	return RegisterObjectPlugin(ID_CIRCLEOBJECT, GeLoadString(IDS_CIRCLE), OBJECT_GENERATOR | OBJECT_ISSPLINE, DoubleCircleData::Alloc, "Odoublecircle"_s, AutoBitmap("circle.tif"_s), 0);
}

@ferdinand

Thanks, Ferdinand! That all makes sense. I was hoping that I was missing something because I had a larger problem that I thought this might have fixed. I'm not sure if I should make a new thread so I'll post my follow up question here.

I simplified my plugin down to just a modification to Double Circle.

SplineObject* DoubleCircleData::GetContour(BaseObject* op, BaseDocument* doc, Float lod, BaseThread* bt)
{
    BaseContainer* bc = op->GetDataInstance();
    if (!bc)
        return nullptr;
    BaseObject* next = op->GetNext();
    if (next==nullptr || next->GetType() != Ospline) // If it's not receiving a valid input return four points
    {
        
        SplineObject* rs = SplineObject::Alloc(4, SPLINETYPE::LINEAR);
        if (!rs)
        {
            return nullptr;
        }
         Vector*     padr = rs->GetPointW();
        padr[0] = Vector(100,0,0);
        padr[1] = Vector(0,100,0);
        padr[2] = Vector(-100,0,0);
        padr[3] = Vector(0,-100,0);
        rs->Message(MSG_UPDATE);
        return rs;
    }
    else if ( next->GetType() == Ospline) // if it's receiving a spline return those points instead
    {
        SplineObject* inputspline = static_cast<SplineObject*>(next);
        Int32 pointcount = inputspline->GetPointCount();
        const Vector * points = inputspline->GetPointR();
        
        SplineObject* rs = SplineObject::Alloc(pointcount, SPLINETYPE::LINEAR);
        if (!rs)
        {
            return nullptr;
        }
        Vector*     padr = rs->GetPointW();
        for (Int32 x = 0; x<pointcount; x++)
        {
            padr[x] = points[x];
        }
        rs->Message(MSG_UPDATE);
        return rs;
    }
    
   
    SplineObject* rs = SplineObject::Alloc(0, SPLINETYPE::LINEAR);
    if (!rs)
    {
        return nullptr;
    }
    rs->Message(MSG_UPDATE);
    return rs;
    
}

The code is pretty simple here, but when I use the Double Circle as an input to a Cloner I get strange results. I have a scene here. Cloner Oddness.c4d

When Spline With Points is after the circle the cloner works correctly, but when I swap the spline to the empty one then instead of getting no output, I get the output as if I didn't have a spline there.

From what I can tell the cloner is rebuilding the cache for the Circle in a different document, where the spline doesn't exist, and that happens to return the four points because of my code.

My question is why is the cloner forcing this rebuild when I return an empty spline to it initially? And is there a way around it. With my plugin there are times when an empty spline is the correct output, but the cloner is forcing unusual rebuilds like in this example.

Dan

Hi @d_schmidt,

no, there is nothing broken with your plugin or Cinema 4D. At least from my understanding and in regard to how Cinema 4D works. There are however some problems with your logic at play:

  1. Your plugin inputs and the frequency with which its cache is being build, i.e., GetContour() is being called.

  2. So let us assume that we have this scene state in your file, where your plugin is working, it copies the points from the node "Spline With Points" next to it.
    0f983a3b-4ddf-4ec7-b60e-b61855e25388-image.png

  3. Then we change the node order of "Spline With Points" and "Empty Spline". The output remains the same, yikes!
    4c807555-88f5-469f-8d55-dbbf34f95a4c-image.png
    Why does that happen? Well, your plugin has no clue that you just changed the the order of the splines and Cinema does call your plugin's GetContour() only when the node itself, "Circle" here, is dirty, not when just something, somewhere changed, as otherwise Cinema would get saturated by cache building rather quickly. So to confirm that, you can just change parameter in your "Circle" and it will then update to the correct status.
    39f2c9b8-c3f1-4ae6-8f1d-b7ec4d15147f-image.png

  4. To fix that need for updating your object manually, you have two options:
    a. Use the flag OBJECT_INPUT when you register your plugin with RegisterObjectPlugin. It has to be passed with the info flag and will cause your plugin to automatically update when its local direct hierarchy, so nodes below it, will be dirty. So it won't work in your case.
    b. Overwrite BaseObject::CheckDirty to manually curate the dirty state of your node. You will find an example for that in the solution below.

There is also the problem that your code has unreachable code (the implicit else) and does not distinguish between the case of a spline with no points and and node that is not a spline, returning the same output for it. Which you probably perceive as faulty output. I also addressed these problems in the comments of the example code below.

If my answer does not address your problem, I would have to ask you in more detail what you meant, especially with the line:

When Spline With Points is after the circle the cloner works correctly, but when I swap the spline to the empty one then instead of getting no output, I get the output as if I didn't have a spline there.

Cheers,
Ferdinand

The file:

/* Modified SDK Double Circle example for evaluating a non-standard dirty state of a node
   via ObjectData::CheckDirty.

   As discussed in:
	plugincafe.maxon.net/topic/13231
*/

#include "c4d.h"
#include "c4d_symbols.h"
#include "odoublecircle.h"
#include "main.h"

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

public:
	virtual Bool Init(GeListNode* node);
	virtual SplineObject* GetContour(BaseObject* op, BaseDocument* doc, Float lod, BaseThread* bt);
	virtual void CheckDirty(BaseObject* op, BaseDocument* doc);

	static NodeData* Alloc() { return NewObjClear(DoubleCircleData); }

private:
	BaseObject* _nextCache = nullptr;
};

Bool DoubleCircleData::Init(GeListNode* node)
{
	BaseObject*		 op = (BaseObject*)node;
	BaseContainer* data = op->GetDataInstance();
	if (!data)
		return false;

	data->SetFloat(CIRCLEOBJECT_RAD, 200.0);
	data->SetInt32(PRIM_PLANE, 0);
	data->SetBool(PRIM_REVERSE, false);
	data->SetInt32(SPLINEOBJECT_INTERPOLATION, SPLINEOBJECT_INTERPOLATION_ADAPTIVE);
	data->SetInt32(SPLINEOBJECT_SUB, 8);
	data->SetFloat(SPLINEOBJECT_ANGLE, DegToRad(5.0));
	data->SetFloat(SPLINEOBJECT_MAXIMUMLENGTH, 5.0);

	return true;
}

// Evaluate the dirty state of our node by checking if its next neighbor has changed.
void DoubleCircleData::CheckDirty(BaseObject* op, BaseDocument* doc)
{	
	// Get the next node to object manager representation of our node and test with
	// the dirty state of our node by comparing it to our cached next node. If they
	// do not match, we ...
	BaseObject* next = op->GetNext();
	if (next != _nextCache) 
	{
		// ... update our cached next node and ...
		_nextCache = next;
		// ... we mark ourselves as dirty (that does not sound right ^^).
		op->SetDirty(DIRTYFLAGS::DATA);
	}
}

SplineObject* DoubleCircleData::GetContour(BaseObject* op, BaseDocument* doc, Float lod, BaseThread* bt)
{
	BaseContainer* bc = op->GetDataInstance();
	if (!bc)
		return nullptr;
	BaseObject* next = op->GetNext();

	// This code is properly executed when you put either no or not a spline object next to the node.
	if (next == nullptr || next->GetType() != Ospline)
	{
		SplineObject* rs = SplineObject::Alloc(4, SPLINETYPE::LINEAR);
		if (!rs)
		{
			return nullptr;
		}
		Vector* padr = rs->GetPointW();
		padr[0] = Vector(100, 0, 0);
		padr[1] = Vector(0, 100, 0);
		padr[2] = Vector(-100, 0, 0);
		padr[3] = Vector(0, -100, 0);
		rs->Message(MSG_UPDATE);
		return rs;
	}

	// In both cases, i.e., the spline with points and without points, your code will branch at runtime 
	// into this condition. For a spline with no points the output will be then of course the same as
	// as your implicit 3rd case (which is unreachable). So if you want to react to empty splines, you
	// have to test fort that.
	else if (next->GetType() == Ospline)
	{
		SplineObject* inputspline = static_cast<SplineObject*>(next);
		Int32 pointcount = inputspline->GetPointCount();
		const Vector* points = inputspline->GetPointR();

		SplineObject* rs = SplineObject::Alloc(pointcount, SPLINETYPE::LINEAR);
		if (!rs)
		{
			return nullptr;
		}
		Vector* padr = rs->GetPointW();
		for (Int32 x = 0; x < pointcount; x++)
		{
			padr[x] = points[x];
		}
		rs->Message(MSG_UPDATE);
		return rs;
	}

	// This code is unreachable due to the fact that case 1 and case 2 cover already all possible
	// outcomes. next can be null when there is no next object, it can be not a spline and it can 
	// be a spline. Since you lump the case of null and not-a-spline together into your case 1, 
	// your case 2 of is-a-spline, then covers all other outcomes. Unless I am overlooking something
	// here.
	SplineObject* rs = SplineObject::Alloc(0, SPLINETYPE::LINEAR);
	if (!rs)
	{
		return nullptr;
	}
	rs->Message(MSG_UPDATE);
	return rs;

}

// be sure to use a unique ID obtained from www.plugincafe.com
#define ID_CIRCLEOBJECT 1001154

Bool RegisterCircle()
{
	return RegisterObjectPlugin(ID_CIRCLEOBJECT, GeLoadString(IDS_CIRCLE), OBJECT_GENERATOR | OBJECT_ISSPLINE, DoubleCircleData::Alloc, "Odoublecircle"_s, AutoBitmap("circle.tif"_s), 0);
}

Its behaviour:
1.gif

Thanks again, Ferdinand!

You're right about the errors in my code, some of them I just left in accidentally because of my rush to respond, sorry!

I didn't properly explain my problem, but after messing around with the differences between your and my code I came across the specific issue.

I added this to CheckDirty:



    Int32 currentframe = doc->GetTime().GetFrame(doc->GetFps());
     // checking if the frame has changed since the last check.
    if (_prevFrame != currentframe)
    {
        _prevFrame = currentframe;
       
        op->SetDirty(DIRTYFLAGS::DATA);
    }

And now when Empty Spline is underneath Circle, four points are returned when there should be nothing.

I don't quite understand why exactly. An extra update seems to be getting forced because of the frame check, but I'm not sure the way around it if I were doing something frame dependent.

All of the code:


#include "c4d.h"
#include "c4d_symbols.h"
#include "odoublecircle.h"
#include "main.h"

class DoubleCircleData : public ObjectData
{
    INSTANCEOF(DoubleCircleData, ObjectData)
    
public:
    virtual Bool Init(GeListNode* node);
    virtual SplineObject* GetContour(BaseObject* op, BaseDocument* doc, Float lod, BaseThread* bt);
    virtual void CheckDirty(BaseObject* op, BaseDocument* doc);
    
    static NodeData* Alloc() { return NewObjClear(DoubleCircleData); }
    
private:
    BaseObject* _nextCache = nullptr;
    Int32 _prevFrame = -1;
};

Bool DoubleCircleData::Init(GeListNode* node)
{
    BaseObject*         op = (BaseObject*)node;
    BaseContainer* data = op->GetDataInstance();
    if (!data)
        return false;
    
    data->SetFloat(CIRCLEOBJECT_RAD, 200.0);
    data->SetInt32(PRIM_PLANE, 0);
    data->SetBool(PRIM_REVERSE, false);
    data->SetInt32(SPLINEOBJECT_INTERPOLATION, SPLINEOBJECT_INTERPOLATION_ADAPTIVE);
    data->SetInt32(SPLINEOBJECT_SUB, 8);
    data->SetFloat(SPLINEOBJECT_ANGLE, DegToRad(5.0));
    data->SetFloat(SPLINEOBJECT_MAXIMUMLENGTH, 5.0);
    
    return true;
}

// Evaluate the dirty state of our node by checking if its next neighbor has changed.
void DoubleCircleData::CheckDirty(BaseObject* op, BaseDocument* doc)
{
    // Get the next node to object manager representation of our node and test with
    // the dirty state of our node by comparing it to our cached next node. If they
    // do not match, we ...
    BaseObject* next = op->GetNext();
    Int32 currentframe = doc->GetTime().GetFrame(doc->GetFps());
    // checking if the frame has changed since the last check.
    if (_prevFrame != currentframe)
    {
        _prevFrame = currentframe;
       
        op->SetDirty(DIRTYFLAGS::DATA);
    }
    if (next != _nextCache)
    {
        // ... update our cached next node and ...
        _nextCache = next;
        // ... we mark ourselves as dirty (that does not sound right ^^).
        op->SetDirty(DIRTYFLAGS::DATA);
    }
}

SplineObject* DoubleCircleData::GetContour(BaseObject* op, BaseDocument* doc, Float lod, BaseThread* bt)
{
    BaseContainer* bc = op->GetDataInstance();
    if (!bc)
        return nullptr;
    BaseObject* next = op->GetNext();
    
    // This code is properly executed when you put either no or not a spline object next to the node.
    if (next == nullptr || next->GetType() != Ospline)
    {
        SplineObject* rs = SplineObject::Alloc(4, SPLINETYPE::LINEAR);
        if (!rs)
        {
            return nullptr;
        }
        Vector* padr = rs->GetPointW();
        padr[0] = Vector(100, 0, 0);
        padr[1] = Vector(0, 100, 0);
        padr[2] = Vector(-100, 0, 0);
        padr[3] = Vector(0, -100, 0);
        rs->Message(MSG_UPDATE);
        return rs;
    }
    //else
    
    // In both cases, i.e., the spline with points and without points, your code will branch at runtime
    // into this condition. For a spline with no points the output will be then of course the same as
    // as your implicit 3rd case (which is unreachable). So if you want to react to empty splines, you
    // have to test fort that.
    SplineObject* inputspline = static_cast<SplineObject*>(next);
    Int32 pointcount = inputspline->GetPointCount();
    const Vector* points = inputspline->GetPointR();
    
    SplineObject* rs = SplineObject::Alloc(pointcount, SPLINETYPE::LINEAR);
    if (!rs)
    {
        return nullptr;
    }
    Vector* padr = rs->GetPointW();
    for (Int32 x = 0; x < pointcount; x++)
    {
        padr[x] = points[x];
    }
    rs->Message(MSG_UPDATE);
    return rs;
    
 
    
}

// be sure to use a unique ID obtained from www.plugincafe.com
#define ID_CIRCLEOBJECT 1001154
Bool RegisterCircle()
{
	return RegisterObjectPlugin(ID_CIRCLEOBJECT, GeLoadString(IDS_CIRCLE), OBJECT_GENERATOR | OBJECT_ISSPLINE, DoubleCircleData::Alloc, "Odoublecircle"_s, AutoBitmap("circle.tif"_s), 0);
}

Thanks for all of the help!

Dan

Hi @d_schmidt,

the problem with you code is that you block yourself out from the event when the node order has been changed in some cases. ObjectData::CheckDirty is being called in the context of EVMSG_CHANGE, i.e., the broad event of something having happened in Cinema 4D. Which includes the advancement of the document time / current frame and the reordering of nodes.

So let us assume the user is at frame 0 in the document:

  1. The user advances the document frame to 1.
  2. EVMSG_CHANGE is invoked, ObjectData::CheckDirtyis being called.
  3. Your implementation is called, _prevFrame != currentframe evaluates as True.
  4. You mark the node as dirty and set _prevFrame = currentframe;
  5. Your GetContour is being called.
  6. Now the user changes the order of the nodes in the scene.
  7. EVMSG_CHANGE is invoked, ObjectData::CheckDirtyis being called.
  8. Your implementation is called, _prevFrame != currentframe evaluates as False because we are still at the same frame.
  9. Your GetContour is not being called.

So the insight here is that CheckDirty is being called a lot and that you have to design your dirty condition carefully in order not to block yourself out when you actually want to update. You can of course also just mark your node as always dirty in CheckDirty, and for a simple single spline plugin this probably will also work just fine, but if every plugin would do that or the user has thousands of instances of your plugin in the scene, this can easily saturate Cinema with building cashes.

Cheers,
Ferdinand

Hi @ferdinand !

I didn't understand it to that degree, but that all seems to make sense and seems to be what's happening here, but I feel like something else is happening here in addition to that. It makes sense that Circle wouldn't update without more checks, but my concern is this.

When the _prevFrame != currentframe is implemented this is the result of having an empty spline underneath circle.

Screen Shot 2021-03-15 at 2.57.59 PM.png

An empty spline is being fed to circle, so there should be no points returned. Instead its running the code:

  
    // This code is properly executed when you put either no or not a spline object next to the node.
    if (next == nullptr || next->GetType() != Ospline)
    {
        SplineObject* rs = SplineObject::Alloc(4, SPLINETYPE::LINEAR);
        if (!rs)
        {
            return nullptr;
        }
        Vector* padr = rs->GetPointW();
        padr[0] = Vector(100, 0, 0);
        padr[1] = Vector(0, 100, 0);
        padr[2] = Vector(-100, 0, 0);
        padr[3] = Vector(0, -100, 0);
        rs->Message(MSG_UPDATE);
        return rs;
    }

So its behaving as if there is no spline next, when there is. This only happens when this code is implemented.

 if (_prevFrame != currentframe)
    {
        _prevFrame = currentframe;
       
        op->SetDirty(DIRTYFLAGS::DATA);
    }

This doesn't seem to make sense to me, although I could be missing something simple.

Thanks for all of the help!
Dan

Hi @d_schmidt,

I have described the reason for why this is happening in principal and more detail in my previous posting. Your GetCountour implementation has nothing to do with this, at least in light of the current information you have given me. The reason for this happening is that you do not call GetCountour often enough, due to a CheckDirty implementation that is not capturing all cases. In this case a reason could be for example that you initialize the field _prevFrameas 0, causing your plugin to fault on frame 0. It also applies here that once you have run through your CheckDirty implementation for a frame all further evaluations for that frame will evaluate as false, because _prevFrame != currentframe is then false. There can be dozens of CheckDirty calls for a single frame and there is no guarantee that on the first call the scene is already in a state you want to update your plugin to.

This ties in to the fact that you are basically trying to implement a design pattern that is not supported by the SDK, namely a generator which is dependent on arbitrary information a scene, here a next node hierarchical relation and the current frame. You have to either design your CheckDirty more carefully or move to a design pattern which is more in line with what ObjectData has been designed for. It might be helpful if you could line out what you are actually trying to do, because I think your current case is still a simplification, right?, so that we can see if what you are trying to do is feasible at all.

Cheers,
Ferdinand

Hi @ferdinand, thank you for your continued patience.

It might be helpful if you could line out what you are actually trying to do, because I think your current case is still a simplification, right?

The rest of my code isn't really relevant here, although it does follow the C4D norms more. Instead of using GetNext(), I use a link field and I have a float that would offset the points from the input spline based on the current frame.

I re-read through your posts but I'm still confused, sorry!

With what you wrote here:

The user advances the document frame to 1.
EVMSG_CHANGE is invoked, ObjectData::CheckDirtyis being called.
Your implementation is called, _prevFrame != currentframe evaluates as True.
You mark the node as dirty and set _prevFrame = currentframe;
Your GetContour is being called.
Now the user changes the order of the nodes in the scene.
EVMSG_CHANGE is invoked, ObjectData::CheckDirtyis being called.
Your implementation is called, _prevFrame != currentframe evaluates as False because we are still at the same frame.
Your GetContour is not being called.

That's not the case is it? I didn't remove the check of if the next object had been changed. So this happens:

Your implementation is called, _prevFrame != currentframe evaluates as False because we are still at the same frame.

But then : next != _nextCache would get checked and that would be true, so GetContour would be called, right? From what I understand adding _prevFrame != currentframe would have GetContour called more, not less, since there is another condition that could trigger it.

It also applies here that once you have run through your CheckDirty implementation for a frame all further evaluations for that frame will evaluate as false, because _prevFrame != currentframe is then false.

While the frame check would always return false, I don't understand why that stops the other check(s) from returning true and GetContour being called correctly. You said there are dozens of calls of CheckDirty, but can only one of them return as dirty and the rest are skipped after that?

Thanks,
Dan

Hi @d_schmidt,

sorry, I did overlook that you only posted a fragment of your CheckDirty, and that your full code is:

void DoubleCircleData::CheckDirty(BaseObject* op, BaseDocument* doc)
{
    BaseObject* next = op->GetNext();
    Int32 currentframe = doc->GetTime().GetFrame(doc->GetFps());
    if (_prevFrame != currentframe)
    {
        _prevFrame = currentframe;
       
        op->SetDirty(DIRTYFLAGS::DATA);
    }
    if (next != _nextCache)
    {
        _nextCache = next;
        op->SetDirty(DIRTYFLAGS::DATA);
    }
}

I cannot reproduce the faulty behavior you mentioned with your code, see gif below which runs your compiled code, when does that happen for you?

1.gif

edit: One thing that just came to mind when looking at your screen above:

alt text

It looks like your spline did update, because I cannot see the spline representation anymore, the only thing that did not seem to have updated is the cloner which clones the spheres onto your spline, it still seems to work on an older cache of your object. Can you confirm that?

Cheers,
Ferdinand

Hi @ferdinand!

In regards to your second screenshot that's correct, that's the issue I've been trying to fix.
I recorded a video before I noticed your edit, hopefully it will clear things up more.

https://www.dropbox.com/s/sp9t21r5n6yeng0/Walkthrough.mp4?dl=0

It looks like your spline did update, because I cannot see the spline representation anymore, the only thing that did not seem to have updated is the cloner which clones the spheres onto your spline, it still seems to work on an older cache of your object. Can you confirm that?

Correct. But I don't think it's an older cache.

From what I can tell by putting some prints in the code it seems like the cloner doesn't want an input with zero points. It seems like the cloner tries rebuilding the cache, but in a virtual document, so the Circle is copied over, but since there is no Next object in this virtual document it returns the default four points.

I'm not sure what the fix here is though. The plugin I'm working on behaves in a similar way where sometimes there would be no points returned, but when the cloner does this it's incorrectly returning points.

Thanks for the help,
Dan

Hi @d_schmidt,

thank for your video, that helped quite a lot. Your conclusion was right, there are multiple documents at play here. In the case of your DoubleCircleData node representation being linked in a Cloner and having a empty spline next to it, the Cloner bugs out and something like visualized by console output below happens:

GetContour() called with document at 00000231f8fd4240
CheckDirty() and dirty requirements met with doc: 00000231a99c1e40 and nextNode: 0000000000000000.
GetContour() called with document at 00000231a99c1e40
CheckDirty() and dirty requirements met with doc: 00000231a99c1e40 and nextNode: 0000000000000000.
GetContour() called with document at 00000231a99c1e40

There are at least two documents at play here and the amount of calls, here three in total, are more than we would expect here. I also had cases where I had five calls in a row or just two. Also the next node in that copied document is not present, i.e., the call op->GetNext() in your GetContour will return a nullptr then; we can see this in line 2 where it says in CheckDirty nextNode: 0000000000000000.

When we instead either do not link your plugin in a cloner or put something else than an empty spline object next to it, the behaviour will then be correct and take a form similar to:

GetContour() called with document at 00000231f8fd4240 and _documentTransferCache at 0000000000000000`

You might be wondering at this point what that _documentTransferCache is. This is part of my little work around I did provide, which basically relies on Noddedata::CopyTo and manually curating this very special case. You can read a bit more about it in the file provided below. At least for me this works now like I would expect it to work. I also did provide a demo file. As stated in the code below, I would however not really recommend doing this, unless you REALLY REALLY need this. My solution is rather hacky, but I do not see another way of fixing the mess the cloner causes. I would either inform my users about that known problem or look for an alternative way to design my plugin. I will report this behaviour of the Cloner as a bug, but it might take some time until someone will fix it.

Cheers,
Ferdinand

The test-scene:
naughty-cloner.c4d
The code:

/*
A rather hacky way to deal with your Cloner & empty spline problem.

I actually would not suggest using this, because this solution is so unnecessarily complicated for a
special corner case that is does not seem to be worth it. This is definitely caused by an, *cough*,
*oddness* of the Cloner object in that special empty spline object case.
	
I would rather suggest to either:
    - Reject empty spline objects as links in your BaseLink.
    - Simply tell your users that this is a known problem and warn them.

While this solution here works, there are so many ifs and buts at play, that it seems rather unlikely
that this will work in every conceivable scenario. I will file a bug report for the Cloner about this,
but due to its niche-case nature I would not hold my breath for a short term fix.

As discussed in:
	https://plugincafe.maxon.net/topic/13231/
*/


#include "c4d.h"
#include "c4d_symbols.h"
#include "odoublecircle.h"
#include "main.h"

#define ID_CIRCLEOBJECT 1001154

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

public:
	virtual Bool Init(GeListNode* node);
	virtual SplineObject* GetContour(BaseObject* op, BaseDocument* doc, Float lod, BaseThread* bt);
	virtual void CheckDirty(BaseObject* op, BaseDocument* doc);
	virtual Bool CopyTo(NodeData* dest, GeListNode* snode, GeListNode* dnode, COPYFLAGS flags, AliasTrans* trn);

	static NodeData* Alloc() { return NewObjClear(DoubleCircleData); }

private:
	BaseObject* _nextNodeTransferCache = nullptr;
	BaseDocument* _docmentTransferCache = nullptr;
	BaseObject* _nextNodeCache = nullptr;
	Int32 _prevFrameCache = 0;
	Bool _hasInitalised = false;
};

Bool DoubleCircleData::Init(GeListNode* node)
{
	BaseObject* op = (BaseObject*)node;
	BaseContainer* data = op->GetDataInstance();
	if (!data)
		return false;

	data->SetFloat(CIRCLEOBJECT_RAD, 200.0);
	data->SetInt32(PRIM_PLANE, 0);
	data->SetBool(PRIM_REVERSE, false);
	data->SetInt32(SPLINEOBJECT_INTERPOLATION, SPLINEOBJECT_INTERPOLATION_ADAPTIVE);
	data->SetInt32(SPLINEOBJECT_SUB, 8);
	data->SetFloat(SPLINEOBJECT_ANGLE, DegToRad(5.0));
	data->SetFloat(SPLINEOBJECT_MAXIMUMLENGTH, 5.0);
	return true;
}
// We are implementing CopyTo to copy over data for that buggy Cloner case.
Bool DoubleCircleData::CopyTo(NodeData* dest, GeListNode* snode, GeListNode* dnode, COPYFLAGS flags, AliasTrans* trn) 
{
	// In case this node is getting copied, we copy over the old document and the next node.
	DoubleCircleData* impl = static_cast<DoubleCircleData*>(dest);

	// This the "source" case, i.e., when we are actually in the document we *want* to be in.
	if (_nextNodeTransferCache == nullptr) 
	{
		impl->_nextNodeTransferCache = _nextNodeCache;
		impl->_docmentTransferCache = snode->GetDocument();
	}
	// Here we are copying a copy, so this will run when our node has been cloned and is then getting cloned again.
	else
	{
		impl->_nextNodeTransferCache = _nextNodeTransferCache;
		impl->_docmentTransferCache = _docmentTransferCache;
	}
	return NodeData::CopyTo(dest, snode, dnode, flags, trn);
}

void DoubleCircleData::CheckDirty(BaseObject* op, BaseDocument* doc)
{
	// The same as yours, it only can also handle documents with a negative document time, specifically frame -1.
	BaseObject* nextNode = op->GetNext();
	Int32 currentFrame = doc->GetTime().GetFrame(doc->GetFps());

	if (!_hasInitalised || nextNode != _nextNodeCache || _prevFrameCache != currentFrame)
	{
		_prevFrameCache = currentFrame;
		_nextNodeCache = nextNode;
		_hasInitalised = true;

		op->SetDirty(DIRTYFLAGS::DATA);
		ApplicationOutput("CheckDirty() and dirty requirements met with doc: @ and nextNode: @.", (void*)doc, (void*)nextNode);
	}
}

SplineObject* DoubleCircleData::GetContour(BaseObject* op, BaseDocument* doc, Float lod, BaseThread* bt)
{
	BaseContainer* bc = op->GetDataInstance();
	if (!bc)
		return nullptr;

	// Some console chirping to see what is going on here. The cloner copies stuff all over the place in case we have an
	// empty spline next to our DoubleCircleData GeListNode representation.
	ApplicationOutput("GetContour() called with document at @ and _documentTransferCache at @", (void*)doc, (void*)_docmentTransferCache);
	ApplicationOutput("_nextNodeCache: @, _nextNodeTransferCache: @.", (void*)_nextNodeCache, (void*)_nextNodeTransferCache);

	// op->GetNext() will "fail" in the case our DoubleCircleData GeListNode representation being linked in a Cloner as a cloning
	// "surface". next will then be a nullptr, although it should point to the empty spline. This has nothing to do with node order or
	// other things one might suspect, but only with the fact if the spline has vertices or not. For a spline with vertices evrything will
	// behave normally.
	// I have not dug around in the depth of the Cloner implementation to see what is happening in detail there, but this would not
        // help us much here for a short term solution anyways.

	// BaseObject* next = op->GetNext();

	// Instead we fall back onto our _nextNodeTransferCache when this document has a _docmentTransferCache which is not null.
	BaseObject* next = nullptr;
	// We have a document transfer cache, so we fall back onto our "actual" node.
	if (_docmentTransferCache != nullptr)
	{
		next = _nextNodeTransferCache;
	}
	// We are in the original document.
	else
	{
		next = op->GetNext();
	}

	if (next == nullptr || next->GetType() != Ospline)
	{
		SplineObject* rs = SplineObject::Alloc(4, SPLINETYPE::LINEAR);
		if (!rs)
		{
			return nullptr;
		}
		Vector* padr = rs->GetPointW();
		padr[0] = Vector(100, 0, 0);
		padr[1] = Vector(0, 100, 0);
		padr[2] = Vector(-100, 0, 0);
		padr[3] = Vector(0, -100, 0);
		rs->Message(MSG_UPDATE);

		//ApplicationOutput("NextNoSplineCondition, returned dummy geom."_s);

		return rs;
	}
	// else

	SplineObject* inputspline = static_cast<SplineObject*>(next);
	Int32 pointcount = inputspline->GetPointCount();
	const Vector* points = inputspline->GetPointR();

	SplineObject* rs = SplineObject::Alloc(pointcount, SPLINETYPE::LINEAR);
	if (!rs)
	{
		return nullptr;
	}
	Vector* padr = rs->GetPointW();
	for (Int32 x = 0; x < pointcount; x++)
	{
		padr[x] = points[x];
	}
	rs->Message(MSG_UPDATE);
	//ApplicationOutput("IsSplineCondition, returned copied spline."_s);
	return rs;

}

Bool RegisterCircle()
{
	return RegisterObjectPlugin(ID_CIRCLEOBJECT, GeLoadString(IDS_CIRCLE), OBJECT_GENERATOR | OBJECT_ISSPLINE, DoubleCircleData::Alloc, "Odoublecircle"_s, AutoBitmap("circle.tif"_s), 0);
}

Hi @ferdinand.

Thanks for all of the help. That all makes sense to me. Hopefully in the future there will be a better work around, but for now I've got a workaround that is clunky but working.

Thanks again,
Dan