For Jeremy (Stable Reference Strings)

Anonymous

For Jeremy (Stable Reference Strings)

Anonymous
Not applicable

I didn't want to keep Hijacking the other thread so I thought I'd make a new one.
As requested by Jeremy, Here's a summary of my investigations into stable reference strings created using Reference.ConvertToStableRepresentation and the Voodoo Magic that they can perform.


A Reference String is a collection of UniqueIds combined with table indecies (delimeted by the ':' character). a reference string for a PlanarFace of a FamilyInstance Element (Accessed through GetSymbolGeometry() )looks like this:

 

66a421da-90cb-4df2-8efe-c312e4f78aa0-0003d32b:0:INSTANCE:a2177cbb-03d8-4416-a4d8-8ada6fc0165a-0003ebf0:78:SURFACE

 

The string obtained from the reference of the same face accessed using GetInstanceGeometry() will look like:

 

a2177cbb-03d8-4416-a4d8-8ada6fc0165a-0003ebf0:78:SURFACE


as you can see this is identical to the last 3 sections of the symbol geometry reference.

the SymbolGeometry reference looks to be 2 references of 3 tokens each

the structured data contained in the string can be used to reliably get at geometry objects that are not completely exposed by the API.
this is done by building a custom string using parts of an existing valid string obtained from the API.

 
The fist step is to tokenise the string:

 

string refString = myReference.ConvertToStableRepresentation(dbDoc);

String[] refTokens = refString.Split(new char[] { ':' });

this will give you an array of string tokens, see below for my interpretation of each token's purpose, I could be wrong on some of these as I've just inferred this information through experimentation (symbol geometry string used as example).

 

token0: 66a421da-90cb-4df2-8efe-c312e4f78aa0-0003d32b - UniqueId of the FamilyInstance Element.
token1: 0 - Presumably an index into a collection within the FamilyInstance (I've not seen anything other than zero here so it could be that the collection always contains a single entry, or that other entries have some other special unknown use)
token2: INSTANCE - signals that the referenced geometry object belongs to A FamilyInstance Element.
token3: a2177cbb-03d8-4416-a4d8-8ada6fc0165a-0003ebf0 - UniqueId of the FamilySymbol Instance that describes the geometry of this FamilyInstance (multiple FamilyInstances that have identical Geometry will reference the same FamilySymbol Instance.
token4: 78 - the index of the referenced geometry object within the FamilySymbol Instance's geometry table. index positions 0 through 8 are reserved for the special reference planes that have been specified in the family editor (explained later).
token5: SURFACE - the type of geometry object referenced by this string.

 

I think the best way to try to follow the hierarchy is to read the tokens in reverse order as follows:

 

The referenced geometry object of type: SURFACE is at index: 78 of the FamilySymbol Instance with UniqueId: a2177cbb-03d8-4416-a4d8-8ada6fc0165a-0003ebf0 as represented by the FamilyInstance with UniqueId: 66a421da-90cb-4df2-8efe-c312e4f78aa0-0003d32b

 

It bothers me that references from geometry objects accessed through GetSymbolGeometry() specify the FamilyInstance while those from GetInstanceGeometry do not, yet the GetInstanceGeometry() objects will be correctly transformed into project coordinates according to the FamilyInstance, where the GetSymbolGeometry() objects are left in Family coordinate space. this seems a little backwards to me.

 

Another strange behaviour is that the ElementId property of the reference from an object returned by GetInstanceGeometry() does not even match the FamilyInstance it came from (it actually refers to the FamilySymbol Instance).

 

 

Now for the Voodoo Magic that I promised.

 

 

If you have a reference in SymbolGeometry format, (as obtained from a Dimension or through the Selection.PickObject method)and you need to get the geometry object in its correct position according to the FamilyInstace, you can tokenise the reference string as demonstrated above and then rebuild it as an InstanceGeometry reference string by discarding the first 3 tokens and joining the remaining 3 using ':' as the delimiter.
you then obtain the InstanceGeometry from the element linked to the original reference and loop through it until you find an object that has a stable reference string that exactly matches your custom made one.

 

public static Edge GetInstanceEdgeFromSymbolRef(Reference symbolRef, Autodesk.Revit.DB.Document dbDoc)
{
	Edge instEdge = null;
	
	Options gOptions = new Options();	
	gOptions.ComputeReferences = true;
	gOptions.DetailLevel= ViewDetailLevel.Undefined;
	gOptions.IncludeNonVisibleObjects = false;
	
	Element elem = dbDoc.GetElement(symbolRef.ElementId);
	string stableRefSymbol = symbolRef.ConvertToStableRepresentation(dbDoc);
	string[] tokenList = stableRefSymbol.Split(new char[] { ':' });
	string stableRefInst = tokenList[3] + ":" + tokenList[4] + ":" + tokenList[5];
	
	GeometryElement geomElem = elem.get_Geometry(gOptions);
	foreach(GeometryObject geomElemObj in geomElem)
	{
		GeometryInstance geomInst = geomElemObj as GeometryInstance;
		if(geomInst != null)
		{
			GeometryElement gInstGeom = geomInst.GetInstanceGeometry();
			foreach(GeometryObject gGeomObject in gInstGeom)
			{
				Solid solid = gGeomObject as Solid;
				if(solid != null)
				{
					foreach(Edge edge in solid.Edges)
					{
						string stableRef = edge.Reference.ConvertToStableRepresentation(dbDoc);
						
						if(stableRef == stableRefInst)
						{
							instEdge = edge;
							break;
						}
					}
				}

				if(instEdge != null)
				{
					// already found, exit early
					break;
				}
			}
		}
		if(instEdge != null)
		{
			// already found, exit early
			break;
		}
	}
	return instEdge;
}

As I mentioned earlier the first 9 positions are occupied by the special references that have been allocated in the family editor the list is as follows:
0 = Left
1 = Center Left/Right
2 = Right
3 = Front
4 = Centre Front/Back
5 = Back
6 = Bottom
7 = Center Elevation
8 = Top

As you can see this sequence matches the order they appear in the family editor reference type drop-down list.

these references are handy for creating Dimensions but the API does not expose them fully (they exist in the element geometry but have the generic type of GeometryObject with no indication of their special purpose other than the index found in the reference string. to get one of these references all you need to do is get any geometry object from the FamilyInstance using GetSymbolGeometry and use it's reference string as a template to build a custom reference that points to the special reference.
you just need to tokenise the reference string and replace token4 with the index value from the list above for the reference you want. token5 should also be Replaced with "SURFACE" but I've found that you can safely leave out the last token and the reference string will still be valid.

to simplify things I created an enumeration for the indecies and a static method that simplifies the process:

 

 

public enum SpecialReferenceType
{
	Left = 0,
	CenterLR = 1,
	Right = 2,
	Front = 3,
	CenterFB = 4,
	Back = 5,
	Bottom = 6,
	CenterElevation = 7,
	Top = 8
}


public static Reference GetSpecialFamilyReference(FamilyInstance inst, SpecialReferenceType refType)
{
	Reference indexRef = null;

	int idx = (int)refType;


	if(inst != null)
	{
		Document dbDoc = inst.Document;

		Options geomOptions = dbDoc.Application.Create.NewGeometryOptions();

		if(geomOptions != null)
		{
			geomOptions.ComputeReferences = true;
			geomOptions.DetailLevel = ViewDetailLevel.Undefined;
			geomOptions.IncludeNonVisibleObjects = true;
		}

		GeometryElement gElement = inst.get_Geometry(geomOptions);
		GeometryInstance gInst = gElement.First() as GeometryInstance;

		String sampleStableRef = null;

		if(gInst != null)
		{
			GeometryElement gSymbol = gInst.GetSymbolGeometry();

			if(gSymbol != null)
			{
				foreach(GeometryObject geomObj in gSymbol)
				{
					if(geomObj is Solid)
					{
						Solid solid = geomObj as Solid;

						if(solid.Faces.Size > 0)
						{
							Face face = solid.Faces.get_Item(0);
							sampleStableRef = face.Reference.ConvertToStableRepresentation(dbDoc);
							break;
						}
					}
					else if(geomObj is Curve)
					{
						Curve curve = geomObj as Curve;

						sampleStableRef = curve.Reference.ConvertToStableRepresentation(dbDoc);
						break;
					}
					else if(geomObj is Point)
					{
						Point point = geomObj as Point;

						sampleStableRef = point.Reference.ConvertToStableRepresentation(dbDoc);
						break;
					}
				}
			}

			if(sampleStableRef != null)
			{
				String[] refTokens = sampleStableRef.Split(new char[] { ':' });

				String customStableRef = refTokens[0] + ":" + refTokens[1] + ":" + refTokens[2] + ":" + refTokens[3] + ":" + idx.ToString();

				indexRef = Reference.ParseFromStableRepresentation(dbDoc, customStableRef);

				GeometryObject geoObj = inst.GetGeometryObjectFromReference(indexRef);

				if(geoObj != null)
				{
					String finalToken = "";

					if(geoObj is Edge)
					{
						finalToken = ":LINEAR";
					}

					if(geoObj is Face)
					{
						finalToken = ":SURFACE";
					}

					customStableRef += finalToken;

					indexRef = Reference.ParseFromStableRepresentation(dbDoc, customStableRef);
				}
				else
				{
					indexRef = null;
				}
			}
		}
		else
		{
			throw new Exception("No Symbol Geometry found...");
		}


	}


	return indexRef;
}

There's a little bit of unnecessary code in that method as it was quickly cobbled together from the code I was using for research, sorry.

 

That's all I have for now. hopefully someone finds it usefull.

 

 

 

 

 

 

Reply
2,279 Views
7 Replies
Replies (7)

jeremytammik
Autodesk
Autodesk
0 Likes

Anonymous
Not applicable

Scott,

I will like to stress the importance of

gOptions.DetailLevel= ViewDetailLevel.Undefined;

I just spent a solid day trying to figure out why your code would work on some families, and not others. Finally, I was able to determine that the family itself was controlling visiblity of faces based on the view detail level. 

 

Strangly enough, when I was blindly troubleshootingthe code I tried setting the following to true.

gOptions.IncludeNonVisibleObjects = true; // false

This still did not work. Maybe I had something else wrong with my code at the time. I don't know.

 

What I've been able to get to work everytime (so far) is the following:

gOptions.DetailLevel = doc.ActiveView.DetailLevel;

To me, this ensures that I can't select a face that isn't visible through a PickObject. 

 

 

Thanks again for all of your voodoo!

0 Likes

jeremytammik
Autodesk
Autodesk

Dear Pat,

 

Thank you very much for pointing out the importance of that.

 

I'll add a note of it to the blog post as well.

 

Later: done:

 

http://thebuildingcoder.typepad.com/blog/2016/04/stable-reference-string-magic-voodoo.html#5

 

Cheers,

 

Jeremy



Jeremy Tammik
Developer Technical Services
Autodesk Developer Network, ADN Open
The Building Coder

0 Likes

Anonymous
Not applicable
I'm a bit confused pat are you saying that using undefined is good or bad?
0 Likes

Anonymous
Not applicable

Lets do some testing...

 

The setup:

I have a structural column (I think its a stock Autodesk family). This family shows straight edges on Medium or Coarse views, and fillet edges on Fine. 

(See attached images).

 

If I try to get the instance face or edge of the above mentioned structural column from the symbol reference (using your code), I get a NullReferenceException error if DetailLevel is set to Undefined:

gOptions.DetailLevel = ViewDetailLevel.Undefined;

Presumably because the PickObject is allowing me to select an edge or face that is non visible (I'm not sure how to confirm this).

The same is also true if I purposely mismatch my View Detail Level and my Options.Detail Level. For example:

Set View Detail Level to Fine

Set Options.DetailLevel to Medium

 

If I try that same code on a family instance where visiblity of edges or faces is not controlled by the view's detail level, then everything works out fine.

 

Looking up this property in the API help file I found the following:

Type: Autodesk.Revit.DB..::..ViewDetailLevel
Value of the detail level. ViewDetailLevel.Undefined means no override is set.

 

My guess is that there is some sort of disconnect between what PickObject is allowing you to select, and the Options class. I should also note that I tested this again to confirm that setting IncludeNonVisibleObjects to true or false made no difference in my test case. 

 

To summarize, if I always make sure my Options.DetailLevel is set to that of my view detail level, then I shouldn't run into any problems (I hope). 

 

 

0 Likes

Anonymous
Not applicable

The Undefined setting was an attempt to make the code more generalised, It worked fine on a few cases I tested it with before posting it, but i might have just got lucky (or unlucky...).I thought that having ViewDetailLevel set to Undefined would provide the full set of geometry from which to find the matching face no-matter what the setting of the view where the selection was made.Maybe that's not how ViewDetailLevel.Undefined works. if it is as you say and it signifies to only return geometry that does not have the detail visibility override set, then to fix it, I would loop through on each ViewDetailLevel setting until the geometry was found. thanks for testing it out and giving a heads-up. btw, which documentation mentions "ViewDetailLevel.Undefined means no override is set"?, I just check and the docs say "View does not use Detail Level " which I think means something completly different to both mine and your interpretations, odd.

out of interest have you changed the code to include the view in the geometry options? if so, does removing it help?

 

0 Likes

Anonymous
Not applicable

Scott, I may have been looking in the wrong location on the api help documents.

I found that snippet of text under OverrideGraphicSettings.SetDetailLevel Method.

 

I am currently using the active view's detail level in my geometry options. For my case its the only reliable way I can ensure that the user doesn't get an exception error. 

gOptions.DetailLevel = doc.ActiveView.DetailLevel;
0 Likes