Tracking BrepFace Changes and Persistent Tracking

Hello!

I am developing a csharp plugin, but we encountered a problem we can seem to solve. We have custom UserData assigned to a Brep, lets call it Space which works great. However, for the Space its relevant for us to be able to track the Surfaces (BrepFaces), because each Face has also UserData, however it is attached to the Space (de-serialized there) and then referenced with the surface index.

I would assume this works still good for minor transformations, moving, scaling ,etc. as I assume the surface index order does not change. But it becomed greatly problematic once we look into SplitFace and other commands that change the geometry greatly.

It would be great to be able to assign a Id or Guid to the BrepFace, in order to track and understand changes and then deploy our handlers. But BrepFace.Id does not seem to presist, not even for simple changes like scale or transform.

Is there a possible solution for this? Did this change for Rhino 8?

I want to be able to explicitly know which surface is which, ideally at all times, or atleast know what is new, removed, etc.

Kind regards,

Ben

Update:

So far, i think i know that most of the times rhino tries to keep the order. Thus even after a splitSurface, a transform, etc. surface index 3 is still surface index 3 and the newly added face is added at the end of the list as surface index 7 for example.

However its getting interesting whne we do boolean operations. Then Rhino, I believe, still tries to keep the surface order, however, it also refills. Thus whne during a boolean union, face index 3 removed, the first newly added face is getting face index 3. Therefore the list does never have an empty space, which does make it difficult for me to track here, what surfaces are added, changed, and which stayed the same.

you can add Userdata on the geometry / Surface / Face Level:
https://developer.rhino3d.com/api/rhinocommon/rhino.geometry.geometrybase/setuserstring

you can detect if stuff has changed with:
https://developer.rhino3d.com/api/rhinocommon/rhino.geometry.geometrybase/datacrc#(uint32)

hope this helps - kind regards - tom

EDIT:
see also your old topic:

1 Like

I have found this aswell and it seems to work maybe for me. However, i was wondering why the data on face.UserData or face.UserDictionary not persist ? Wouldnt one of those be more meant for this use case?

Kind regards,

Ben

what are you doing / what action makes the data to be lost ?

This is what i am doing, the UserDataDictionary seems to persist, the UserData doesnt event get applied?


protected override Result RunCommandLogic(RhinoDoc doc)
{
var go = new GetObject();
go.GeometryFilter = ObjectType.Surface;
go.SubObjectSelect = true;
go.SetCommandPrompt(“Select brep-face”);
go.Get();

if (go.CommandResult() != Result.Success)
    return go.CommandResult();

ObjRef objRef = go.Object(0);
var brep = objRef.Brep();
if (brep == null)
    return Result.Failure;

var key = "ThisIsTestKey";

string text = string.Empty;
var rc = RhinoGet.GetString("Text", false, ref text);
if (rc != Result.Success)
    return rc;

int faceIndex = objRef.GeometryComponentIndex.Index;
if (faceIndex >= 0 && faceIndex < brep.Faces.Count)
{
    // Testing UserString
    brep.Faces[faceIndex].SetUserString(key, text);

    // Testing UserDictionary
    brep.Faces[faceIndex].UserDictionary.Set(key, text);

    // Testing UserData
    var faceUD = new FaceUserData();
    faceUD.Text = text;

    brep.Faces[faceIndex].UserData.Add(faceUD);

    doc.Objects.Replace(objRef.ObjectId, brep);
    doc.Views.Redraw();
    return Result.Success;
}

return Result.Failure;
}

with

public class FaceUserData : Rhino.DocObjects.Custom.UserData
{
    public string Text { get; set; }

    public FaceUserData()
    { }

    public FaceUserData(string text) => Text = text;

    public override bool ShouldWrite => true;

    protected override bool Write(BinaryArchiveWriter writer)
    {
        writer.WriteString(Text ?? string.Empty);
        return true;
    }

    protected override bool Read(BinaryArchiveReader reader)
    {
        Text = reader.ReadString();
        return true;
    }

    public override string ToString() => $"FaceUserData: {Text}";
}

and then i check using:

protected override Result RunCommandLogic(RhinoDoc doc)
{
var go = new GetObject
{
GeometryFilter = ObjectType.Brep | ObjectType.Surface,
SubObjectSelect = false
};
go.SetCommandPrompt(“Select Breps or surfaces”);
go.EnableUnselectObjectsOnExit(false);
go.GetMultiple(1, 0);

if (go.CommandResult() != Result.Success) return go.CommandResult();
if (go.ObjectCount < 1) return Result.Cancel;

foreach (var objRef in go.Objects())
{
    var rhObj = objRef.Object();
    if (rhObj == null) continue;

    Brep brep = rhObj.Geometry as Brep ?? (rhObj.Geometry as Surface)?.ToBrep();
    if (brep == null)
    {
        RhinoApp.WriteLine($"Object {rhObj.Id} cannot be converted to a Brep. Skipping.");
        continue;
    }

    RhinoApp.WriteLine($"Checking Object {rhObj.Id} {rhObj.Name}");

    for (int i = 0; i < brep.Faces.Count; i++)
    {
        var face = brep.Faces[i];

        // UserStrings
        var strings = face.GetUserStrings();
        var userStringPairs = strings?.AllKeys
            .Where(k => k != null)
            .Select(k => $"{k}={strings[k]}")
            .DefaultIfEmpty("<none>")
            ?? new[] { "<none>" };

        // UserDictionary
        var dictPairs = face.UserDictionary.Keys
            .Cast<string>()
            .Select(k => $"{k}={face.UserDictionary[k]}")
            .DefaultIfEmpty("<none>");

        // Custom UserData
        var userDataEntries = face.UserData
            .OfType<FaceUserData>()
            .Select(d => $"FaceUserData={d.Text}")
            .DefaultIfEmpty("<none>");

        string line =
            $"Object {rhObj.Id} - Face {i} | " +
            $"UserStrings: [{string.Join(", ", userStringPairs)}] | " +
            $"UserDictionary: [{string.Join(", ", dictPairs)}] | " +
            $"UserData: [{string.Join(", ", userDataEntries)}]";

        RhinoApp.WriteLine(line);
    }
}

doc.Views.Redraw();
return Result.Success;

}

Do i also necessarly need to use replace when assigning the UserData, UserDataDictionary or UserDataString?

Gladly appreciate your support here @Tom_P !

UserData would be great to add here, because then i can simply hook up my class to the BrepFace, instead of serializing it from heigher up, like the space, thus Brep.

Otherwise i think the idea is, to assing a Guid into the Dict or the UserDataStrings, serving as an identifier afterwards to reassign and match my UserData with a BrepFace again, since i cannot trust the brepface order or numbering. I dont think i want to use the BrepFace.Id, as this might be overwritten by annother plugin or system.

and one other issue i have is, that I havet to use replace the object.

The procedure currently is, i collect on Event such as onAdd, onReplace, etc. Guids and then i check those on Idle, and adjust my UserData accordingly. That means though that i cannot use replace, i am supressing all those events duing on idle. Therefor using replace during on idle, would cause during an undo a “duplication” / it keeps the newly replaced object and then the old object from undo.

Is my system flawed? Is there a better approach to this?

what happens if you check the Result of SetUserString ?
by persist - do you mean within one rhinosession - or closing and reopening the doc ?

        bool setUSResult = brep.Faces[faceIndex].SetUserString(key, text);
        RhinoApp.WriteLine($"SetUserString {(setUSResult ? "Success" : "FAILED")}");

Maybe the underlaying surfaces of the brep are better Elements to attach the Data.

not sure if i got this. just use a flag to not listen to the events you trigger ?

CommitChanges may also work here instead of replace…

1 Like

I made some progress, I am using the UserDataDictionary now assigning a custom Guid, which makes it possible to identify and refernce back correct custom UserData from my Side back to the correct Surface Indecies after any Rhino Changes. Just need to ad some more checks here on my side and edge cases I think.

I got a flag set with the supression for the events and that works. However the problem is the following:

I have a Brep with SpaceUserData, where each Face has FaceUserData. FaceUserData is stored inside the SpaceUserData and is refernceing via SurfaceIndex the correct face.

FYI: I use face.PerFaceColor to set colors based on Properties insdie the FaceUserData.

Now with a properly setup Space, I use for example SplitFace. I am able to understand that an additional face was added, and i can reassing a duplicate of FaceUserData of a adjacent similar face. That all works. However now, the Brep is not properly colored anymore so i would like to do a recoloring.

So i use face.PerFaceColor again and CommitChanges() to the rhinoObject, as I cant simple redraw the viewport. (I currently simply assing random colors for testing)

This is all ok, until I now to an Undo of the SplitFace action, which is the last Undo record in my Undo stack. This leaves 2 Breps now, both with the same SpaceUserData. one is the old object, the other one is the new, on top of each other.

maybe this helps:

https://developer.rhino3d.com/api/rhinocommon/rhino.rhinodoc/beginundorecord

https://developer.rhino3d.com/api/rhinocommon/rhino.rhinodoc/addcustomundoevent

I am using these however this wont work in this case since I am using Rhinos Edit commands, like SplitSurface, MergeCoPlanarFaces, BooleanDifference, etc.

I could simply color the objects after and register another undo, but then I would have 2 step backwards process, by first undoing the color update and then undo the Rhino edit like SplitSurface, etc.

this might be ok if your plug-in is for a personal / custom workflow.

I am not an expert in juggling with undo / redo.
there is some stuff arround and exposed to rhinocommon
https://developer.rhino3d.com/api/rhinocommon/rhino.commands.command/undoredo
https://developer.rhino3d.com/api/rhinocommon/rhino.rhinodoc/currentundorecordserialnumber

you might be able to trigger the second undo.

but really - i am just guessing.

@dale might be the right person to ask.

kind regards -tom

1 Like

Hi @Benterich,

Full disclosure - I have not read this entire thread.

As you know, Brep faces do not have a unique ids. However, using user data you can add your own unique ids.

Note, a BrepFace is a proxy surface. So, it acts on behalf of its underlying surface. This is where you need to attach any user data.

Please review the attached and let me know if you have any questions.

TestBrepFaceUserData.cs (3.3 KB)

– Dale

2 Likes

Hi @Dale ! thanks for the help! I tried to attach the UserData to the BrepFace proxy … what a stupid mistake. Attaching it to the Surface works greatly and I have implemented it.

Now the only “problem” I still have is the Replace, but I think there is no way around it, right? If I attach UserData to a Surface, i have to use Replace in order to have it updated. As described earlier, I am doing a lot of solving on Idle, thus during events I am collecting obj that have changed, and then handle them on Idle, depending on the case. Since I use PerFaceColoring and UserData on Surfaces, I have to replace the Object in order to update it. However, when I do this on Idle, I have to register another Undo, thus the User needs to do in order to go back from for example using Rhino SplitSurface, he would need to undo my custom event, and then the SplitFace.

Do you have any idea to work around this?

Like, is it possible to attach additional history to the last event for example?

Can i keep the the undo Stack registering changes after onIdle?

Thanks for the help!

I have found a solution! I use OnCommandEnd, this way my changes are still in the correct Undo Stack ! :blush:

Thus the solution is to use store UserData onto the Surfaces and then when OnCommand End has run, I apply my checks and changes, after collecting all edited Objects via the Events OnReplace, OnModify, etc.

Thanks @dale & @Tom_P for taking the time to help! Much appreciated!

This was giving me headaches for months .. glad I finally have a proper solution.

1 Like

One more quick question. Is it common practice to attach Userdata to the RhinoObject.Attributes.UserData, or to the Brep.UserData of the rhinoObject?

I think the more commonly recommended practice is to attach to the Attributes - since that’s a property of the RhinoObject, i.e. the data which is actually saved in the 3dm file. The Brep.UserData is attached to the geometry, the “other half” of the RhinoObject (vis-a-vis the Attributes). I suppose it depends conceptually on what you’re doing if you want to use the UserData in the Geometry or the Attributes.

Yes, there is a lot to keep track of there for undo/redo, and especially UndeleteRhinoObject.

By the way, if you use attributes, you can duplicate the attributes (Attributes.Duplicate()) then make your modification and pass the new attributes to the object with RhinoDoc.Objects.ModifyAttributes(obj, newattr, true) which will trigger the RhinoDoc.ModifyObjectAttributes event which you can then work with in the IdleHandler.

1 Like

Hi @Benterich,

In general, user data survives “better” when attached to an object’s attributes. Your mileage may vary.

– Dale

1 Like